use base64::Engine;
use clap::{Parser, Subcommand};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process;

const GIT_COMMIT: &str = env!("GIT_COMMIT");

// ── Config ──────────────────────────────────────────────────────────────────

struct Config {
    base_url: String,
    api_key: String,
}

fn load_config() -> Config {
    let exe = std::env::current_exe().unwrap_or_default();
    let dir = exe.parent().unwrap_or(Path::new("."));
    let path = dir.join("config.env");

    let mut map = HashMap::new();
    if let Ok(content) = std::fs::read_to_string(&path) {
        for line in content.lines() {
            let line = line.trim();
            if line.is_empty() || line.starts_with('#') {
                continue;
            }
            if let Some((k, v)) = line.split_once('=') {
                map.insert(k.trim().to_string(), v.trim().to_string());
            }
        }
    }

    let base_url = map
        .get("HELIOS_REMOTE_URL")
        .cloned()
        .unwrap_or_default()
        .trim_end_matches('/')
        .to_string();
    let api_key = map
        .get("HELIOS_REMOTE_API_KEY")
        .cloned()
        .unwrap_or_default();

    if base_url.is_empty() || api_key.is_empty() {
        eprintln!(
            "[helios-remote] ERROR: config.env missing or incomplete at {}",
            path.display()
        );
        process::exit(1);
    }

    Config { base_url, api_key }
}

// ── HTTP helpers ────────────────────────────────────────────────────────────

fn client() -> reqwest::blocking::Client {
    reqwest::blocking::Client::new()
}

fn req(
    cfg: &Config,
    method: &str,
    path: &str,
    body: Option<Value>,
    timeout_secs: u64,
) -> Value {
    let url = format!("{}{}", cfg.base_url, path);
    let c = client();
    let timeout = std::time::Duration::from_secs(timeout_secs);

    let builder = match method {
        "GET" => c.get(&url),
        "POST" => c.post(&url),
        _ => c.get(&url),
    };

    let builder = builder
        .header("X-Api-Key", &cfg.api_key)
        .header("Content-Type", "application/json")
        .timeout(timeout);

    let builder = if let Some(b) = body {
        builder.body(b.to_string())
    } else {
        builder
    };

    let resp = match builder.send() {
        Ok(r) => r,
        Err(e) => {
            if e.is_timeout() {
                eprintln!(
                    "[helios-remote] TIMEOUT: {} did not respond within {} s",
                    url, timeout_secs
                );
            } else {
                eprintln!(
                    "[helios-remote] CONNECTION ERROR: Cannot reach {}\n  → {}",
                    url, e
                );
            }
            process::exit(1);
        }
    };

    let status = resp.status();
    if !status.is_success() {
        let body_text = resp.text().unwrap_or_default();
        let body_preview = if body_text.len() > 1000 {
            &body_text[..1000]
        } else {
            &body_text
        };
        eprintln!(
            "[helios-remote] HTTP {} {}\n  URL    : {}\n  Method : {}\n  Body   : {}",
            status.as_u16(),
            status.canonical_reason().unwrap_or(""),
            url,
            method,
            body_preview,
        );
        process::exit(1);
    }

    resp.json::<Value>().unwrap_or(json!({}))
}

// ── Label validation ────────────────────────────────────────────────────────

fn validate_label(label: &str) {
    let valid = !label.is_empty()
        && label
            .chars()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
        && label.chars().next().unwrap().is_ascii_alphanumeric();
    if !valid {
        eprintln!(
            "[helios-remote] Invalid label '{}'. Must be lowercase, no whitespace, only a-z 0-9 - _",
            label
        );
        process::exit(1);
    }
}

// ── Window resolution ───────────────────────────────────────────────────────

fn resolve_window(cfg: &Config, device: &str, window_label: &str) -> i64 {
    let data = req(cfg, "GET", &format!("/devices/{}/windows", device), None, 30);
    let windows = data["windows"].as_array().cloned().unwrap_or_default();
    let query = window_label.to_lowercase();

    // Exact label match
    for w in &windows {
        if w["visible"].as_bool() == Some(true) && w["label"].as_str() == Some(&query) {
            return w["id"].as_i64().unwrap();
        }
    }

    // Substring match on label
    let mut matches: Vec<&Value> = windows
        .iter()
        .filter(|w| {
            w["visible"].as_bool() == Some(true)
                && w["label"]
                    .as_str()
                    .map(|l| l.contains(&query))
                    .unwrap_or(false)
        })
        .collect();

    // Fallback: substring on title
    if matches.is_empty() {
        matches = windows
            .iter()
            .filter(|w| {
                w["visible"].as_bool() == Some(true)
                    && w["title"]
                        .as_str()
                        .map(|t| t.to_lowercase().contains(&query))
                        .unwrap_or(false)
            })
            .collect();
    }

    if matches.is_empty() {
        eprintln!(
            "[helios-remote] No visible window matching '{}'",
            window_label
        );
        process::exit(1);
    }

    if matches.len() > 1 {
        println!(
            "[helios-remote] Multiple matches for '{}', using first:",
            window_label
        );
        for w in &matches {
            println!(
                "  {:<30}  {}",
                w["label"].as_str().unwrap_or("?"),
                w["title"].as_str().unwrap_or("")
            );
        }
    }

    matches[0]["id"].as_i64().unwrap()
}

// ── CLI ─────────────────────────────────────────────────────────────────────

#[derive(Parser)]
#[command(
    name = "helios",
    about = "Control devices connected to the Helios Remote Relay Server."
)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// List all connected devices
    Devices,

    /// Capture screenshot (screen or window label)
    Screenshot {
        /// Device label
        device: String,
        /// 'screen' for full screen, or a window label
        target: String,
    },

    /// Run a shell command on the remote device
    Exec {
        /// Device label
        device: String,
        /// Timeout in seconds
        #[arg(long)]
        timeout: Option<u64>,
        /// Command (and arguments) to execute
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        parts: Vec<String>,
    },

    /// List all visible windows on the remote device
    Windows {
        /// Device label
        device: String,
    },

    /// Bring a window to the foreground
    Focus {
        /// Device label
        device: String,
        /// Window label
        window: String,
    },

    /// Maximize and focus a window
    Maximize {
        /// Device label
        device: String,
        /// Window label
        window: String,
    },

    /// Minimize all windows
    MinimizeAll {
        /// Device label
        device: String,
    },

    /// Show a notification to the user (fire-and-forget, no response needed)
    Inform {
        /// Device label
        device: String,
        /// Message to display
        message: String,
        /// Custom dialog title
        #[arg(long)]
        title: Option<String>,
    },

    /// Launch a program (fire-and-forget)
    Run {
        /// Device label
        device: String,
        /// Program to launch
        program: String,
        /// Program arguments
        #[arg(trailing_var_arg = true)]
        args: Vec<String>,
    },

    /// Get clipboard contents
    ClipboardGet {
        /// Device label
        device: String,
    },

    /// Set clipboard contents
    ClipboardSet {
        /// Device label
        device: String,
        /// Text to set
        text: String,
    },

    /// Upload a local file to the remote device
    Upload {
        /// Device label
        device: String,
        /// Local file path
        local_path: PathBuf,
        /// Remote file path
        remote_path: String,
    },

    /// Download a file from the remote device
    Download {
        /// Device label
        device: String,
        /// Remote file path
        remote_path: String,
        /// Local file path
        local_path: PathBuf,
    },

    /// Compare relay, CLI, and client commits
    Version {
        /// Device label
        device: String,
    },

    /// Fetch last N lines of client log file
    Logs {
        /// Device label
        device: String,
        /// Number of lines
        #[arg(long, default_value = "20")]
        lines: u32,
    },
}

fn main() {
    let cli = Cli::parse();
    let cfg = load_config();

    match cli.command {
        Commands::Devices => {
            let data = req(&cfg, "GET", "/devices", None, 30);
            let devices = data["devices"].as_array();
            match devices {
                Some(devs) if !devs.is_empty() => {
                    println!("{:<30}", "Device");
                    println!("{}", "-".repeat(30));
                    for d in devs {
                        println!("{}", d["label"].as_str().unwrap_or("?"));
                    }
                }
                _ => println!("No devices connected."),
            }
        }

        Commands::Screenshot { device, target } => {
            validate_label(&device);
            let out_path = Path::new("/tmp/helios-remote-screenshot.png");

            let data = if target == "screen" {
                req(
                    &cfg,
                    "POST",
                    &format!("/devices/{}/screenshot", device),
                    None,
                    30,
                )
            } else {
                let wid = resolve_window(&cfg, &device, &target);
                req(
                    &cfg,
                    "POST",
                    &format!("/devices/{}/windows/{}/screenshot", device, wid),
                    None,
                    30,
                )
            };

            let b64 = data["image_base64"]
                .as_str()
                .or(data["screenshot"].as_str())
                .or(data["image"].as_str())
                .or(data["data"].as_str())
                .or(data["png"].as_str());

            let b64 = match b64 {
                Some(s) => s,
                None => {
                    eprintln!("[helios-remote] ERROR: No image in response.");
                    process::exit(1);
                }
            };

            let b64 = if let Some((_, after)) = b64.split_once(',') {
                after
            } else {
                b64
            };

            let bytes = base64::engine::general_purpose::STANDARD
                .decode(b64)
                .unwrap_or_else(|e| {
                    eprintln!("[helios-remote] ERROR: Failed to decode base64: {}", e);
                    process::exit(1);
                });

            std::fs::write(out_path, &bytes).unwrap_or_else(|e| {
                eprintln!("[helios-remote] ERROR: Failed to write screenshot: {}", e);
                process::exit(1);
            });
            println!("{}", out_path.display());
        }

        Commands::Exec {
            device,
            timeout,
            parts,
        } => {
            validate_label(&device);
            let command = parts.join(" ");
            let mut body = json!({"command": command});
            if let Some(t) = timeout {
                body["timeout_ms"] = json!(t * 1000);
            }
            let http_timeout = timeout.unwrap_or(30) + 5;
            let data = req(
                &cfg,
                "POST",
                &format!("/devices/{}/exec", device),
                Some(body),
                http_timeout.max(35),
            );

            let stdout = data["stdout"]
                .as_str()
                .or(data["output"].as_str())
                .unwrap_or("");
            let stderr = data["stderr"].as_str().unwrap_or("");
            let exit_code = data["exit_code"].as_i64();

            if !stdout.is_empty() {
                if stdout.ends_with('\n') {
                    print!("{}", stdout);
                } else {
                    println!("{}", stdout);
                }
            }
            if !stderr.is_empty() {
                eprintln!("[stderr] {}", stderr);
            }
            if let Some(code) = exit_code {
                if code != 0 {
                    eprintln!("[helios-remote] Command exited with code {}", code);
                    process::exit(code as i32);
                }
            }
        }

        Commands::Windows { device } => {
            validate_label(&device);
            let data = req(
                &cfg,
                "GET",
                &format!("/devices/{}/windows", device),
                None,
                30,
            );
            let windows = data["windows"].as_array().cloned().unwrap_or_default();
            let visible: Vec<&Value> = windows
                .iter()
                .filter(|w| w["visible"].as_bool() == Some(true))
                .collect();
            if visible.is_empty() {
                println!("No windows returned.");
                return;
            }
            println!("{:<30}  Title", "Label");
            println!("{}", "-".repeat(70));
            for w in visible {
                println!(
                    "{:<30}  {}",
                    w["label"].as_str().unwrap_or("?"),
                    w["title"].as_str().unwrap_or("")
                );
            }
        }

        Commands::Focus { device, window } => {
            validate_label(&device);
            let wid = resolve_window(&cfg, &device, &window);
            req(
                &cfg,
                "POST",
                &format!("/devices/{}/windows/{}/focus", device, wid),
                None,
                30,
            );
            println!("Window '{}' focused on {}.", window, device);
        }

        Commands::Maximize { device, window } => {
            validate_label(&device);
            let wid = resolve_window(&cfg, &device, &window);
            req(
                &cfg,
                "POST",
                &format!("/devices/{}/windows/{}/maximize", device, wid),
                None,
                30,
            );
            println!("Window '{}' maximized on {}.", window, device);
        }

        Commands::MinimizeAll { device } => {
            validate_label(&device);
            req(
                &cfg,
                "POST",
                &format!("/devices/{}/windows/minimize-all", device),
                None,
                30,
            );
            println!("All windows minimized on {}.", device);
        }

        Commands::Inform {
            device,
            message,
            title,
        } => {
            validate_label(&device);
            let mut body = json!({"message": message});
            if let Some(t) = title {
                body["title"] = json!(t);
            }
            req(
                &cfg,
                "POST",
                &format!("/devices/{}/inform", device),
                Some(body),
                10,
            );
            println!("User informed on {}.", device);
        }

        Commands::Run {
            device,
            program,
            args,
        } => {
            validate_label(&device);
            req(
                &cfg,
                "POST",
                &format!("/devices/{}/run", device),
                Some(json!({"program": program, "args": args})),
                30,
            );
            println!("Started {:?} on {}.", program, device);
        }

        Commands::ClipboardGet { device } => {
            validate_label(&device);
            let data = req(
                &cfg,
                "GET",
                &format!("/devices/{}/clipboard", device),
                None,
                30,
            );
            println!("{}", data["text"].as_str().unwrap_or(""));
        }

        Commands::ClipboardSet { device, text } => {
            validate_label(&device);
            req(
                &cfg,
                "POST",
                &format!("/devices/{}/clipboard", device),
                Some(json!({"text": text})),
                30,
            );
            println!("Clipboard set ({} chars) on {}.", text.len(), device);
        }

        Commands::Upload {
            device,
            local_path,
            remote_path,
        } => {
            validate_label(&device);
            if !local_path.exists() {
                eprintln!(
                    "[helios-remote] ERROR: Local file not found: {}",
                    local_path.display()
                );
                process::exit(1);
            }
            let bytes = std::fs::read(&local_path).unwrap_or_else(|e| {
                eprintln!("[helios-remote] ERROR: Failed to read file: {}", e);
                process::exit(1);
            });
            let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
            req(
                &cfg,
                "POST",
                &format!("/devices/{}/upload", device),
                Some(json!({"path": remote_path, "content_base64": b64})),
                30,
            );
            println!(
                "Uploaded {} → {} on {}.",
                local_path.display(),
                remote_path,
                device
            );
        }

        Commands::Download {
            device,
            remote_path,
            local_path,
        } => {
            validate_label(&device);
            let encoded = urlencoding::encode(&remote_path);
            let data = req(
                &cfg,
                "GET",
                &format!("/devices/{}/download?path={}", device, encoded),
                None,
                30,
            );
            let b64 = data["content_base64"].as_str().unwrap_or("");
            if b64.is_empty() {
                eprintln!("[helios-remote] ERROR: No content in download response.");
                process::exit(1);
            }
            let bytes = base64::engine::general_purpose::STANDARD
                .decode(b64)
                .unwrap_or_else(|e| {
                    eprintln!("[helios-remote] ERROR: Failed to decode base64: {}", e);
                    process::exit(1);
                });
            if let Some(parent) = local_path.parent() {
                std::fs::create_dir_all(parent).ok();
            }
            std::fs::write(&local_path, &bytes).unwrap_or_else(|e| {
                eprintln!("[helios-remote] ERROR: Failed to write file: {}", e);
                process::exit(1);
            });
            let size = data["size"].as_u64().unwrap_or(bytes.len() as u64);
            println!(
                "Downloaded {} → {} ({} bytes).",
                remote_path,
                local_path.display(),
                size
            );
        }

        Commands::Version { device } => {
            validate_label(&device);

            // Relay version
            let relay_commit = match reqwest::blocking::get(&format!("{}/version", cfg.base_url)) {
                Ok(r) => r
                    .json::<Value>()
                    .ok()
                    .and_then(|v| v["commit"].as_str().map(String::from))
                    .unwrap_or_else(|| "?".into()),
                Err(e) => format!("{}", e),
            };

            // CLI commit
            let cli_commit = GIT_COMMIT;

            // Client version
            let client_commit =
                match std::panic::catch_unwind(|| {
                    req(&cfg, "GET", &format!("/devices/{}/version", device), None, 10)
                }) {
                    Ok(data) => data["commit"]
                        .as_str()
                        .unwrap_or("?")
                        .to_string(),
                    Err(_) => "unreachable".to_string(),
                };

            let all_same = relay_commit == cli_commit && cli_commit == client_commit;
            println!("  relay      {}", relay_commit);
            println!("  cli        {}", cli_commit);
            println!("  client     {}", client_commit);
            println!(
                "  {}",
                if all_same {
                    "✅ all in sync"
                } else {
                    "⚠️  OUT OF SYNC"
                }
            );
        }

        Commands::Logs { device, lines } => {
            validate_label(&device);
            let data = req(
                &cfg,
                "GET",
                &format!("/devices/{}/logs?lines={}", device, lines),
                None,
                30,
            );
            if let Some(err) = data["error"].as_str() {
                eprintln!("[helios-remote] {}", err);
                process::exit(1);
            }
            println!(
                "# {}  (last {} lines)",
                data["log_path"].as_str().unwrap_or("?"),
                lines
            );
            println!("{}", data["content"].as_str().unwrap_or(""));
        }
    }
}
