use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashSet;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use tauri::{Emitter, Manager};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, ChildStdin, ChildStdout, Command};
use tokio::sync::Mutex;

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct CommandEnvelope {
    action: String,
    #[serde(default)]
    correlation_id: Option<String>,
    #[serde(default)]
    id: Option<String>,
    #[serde(default)]
    r#type: Option<String>,
    #[serde(default)]
    tag: Option<String>,
    #[serde(default)]
    payload: Option<Value>,
}

const DEFAULT_SETTINGS_TAGS: &[&str] = &["Personal", "Work", "Ideas", "Journal"];
const DEFAULT_FRAGMENT_TYPES: &[&str] = &["Quote", "Snippet", "Reference"];
const DEFAULT_STARTUP_VIEW: &str = "entries";

#[derive(Deserialize, Serialize)]
struct AppSettings {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    sidecar_root: Option<String>,
    #[serde(default = "default_settings_tags")]
    tags: Vec<String>,
    #[serde(default = "default_fragment_types")]
    fragment_types: Vec<String>,
    #[serde(default = "default_startup_view")]
    default_startup_view: String,
}

impl Default for AppSettings {
    fn default() -> Self {
        Self {
            sidecar_root: None,
            tags: default_settings_tags(),
            fragment_types: default_fragment_types(),
            default_startup_view: default_startup_view(),
        }
    }
}

fn default_settings_tags() -> Vec<String> {
    DEFAULT_SETTINGS_TAGS
        .iter()
        .map(|v| (*v).to_string())
        .collect()
}

fn default_fragment_types() -> Vec<String> {
    DEFAULT_FRAGMENT_TYPES
        .iter()
        .map(|v| (*v).to_string())
        .collect()
}

fn default_startup_view() -> String {
    DEFAULT_STARTUP_VIEW.to_string()
}

fn normalize_items(values: Vec<String>, fallback: &[&str]) -> Vec<String> {
    let mut seen = HashSet::new();
    let mut normalized = Vec::new();
    for item in values {
        let trimmed = item.trim();
        if trimmed.is_empty() {
            continue;
        }
        let key = trimmed.to_lowercase();
        if seen.insert(key) {
            normalized.push(trimmed.to_string());
        }
    }

    if normalized.is_empty() {
        return fallback.iter().map(|v| (*v).to_string()).collect();
    }

    normalized
}

fn normalize_startup_view(value: Option<String>) -> String {
    let normalized = value
        .unwrap_or_else(default_startup_view)
        .trim()
        .to_lowercase();
    match normalized.as_str() {
        "entries" | "calendar" | "fragments" | "todos" | "lists" => normalized,
        _ => default_startup_view(),
    }
}

struct ManagedSidecar {
    child: Child,
    stdin: ChildStdin,
    stdout: BufReader<ChildStdout>,
}

struct ManagedSpeechProcess {
    poll_task: tokio::task::JoinHandle<()>,
}

impl ManagedSidecar {
    fn start(root: &Path, resource_dir: Option<&Path>) -> Result<Self, String> {
        let sidecar_path = resolve_sidecar_path(root, resource_dir)?;
        let mut cmd = Command::new(sidecar_path);
        cmd.stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::inherit())
            .current_dir(root)
            .env("JOURNAL_PROJECT_ROOT", root)
            .kill_on_drop(true);
        #[cfg(windows)]
        cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
        let mut child = cmd
            .spawn()
            .map_err(|err| format!("Failed to start sidecar process: {err}"))?;

        let stdin = child
            .stdin
            .take()
            .ok_or_else(|| "Unable to open sidecar stdin.".to_string())?;
        let stdout = child
            .stdout
            .take()
            .ok_or_else(|| "Unable to open sidecar stdout.".to_string())?;

        Ok(Self {
            child,
            stdin,
            stdout: BufReader::new(stdout),
        })
    }

    fn is_running(&mut self) -> bool {
        match self.child.try_wait() {
            Ok(None) => true,
            Ok(Some(_)) => false,
            Err(_) => false,
        }
    }

    async fn send_command_line(&mut self, input_line: &str) -> Result<String, String> {
        self.stdin
            .write_all(format!("{input_line}\n").as_bytes())
            .await
            .map_err(|err| format!("Failed writing to sidecar stdin: {err}"))?;
        self.stdin
            .flush()
            .await
            .map_err(|err| format!("Failed flushing sidecar stdin: {err}"))?;

        let mut response_line = String::new();
        let read = self
            .stdout
            .read_line(&mut response_line)
            .await
            .map_err(|err| format!("Failed reading sidecar stdout: {err}"))?;
        if read == 0 {
            return Err("Sidecar stdout closed unexpectedly.".to_string());
        }

        let trimmed = response_line.trim().to_string();
        if trimmed.is_empty() {
            return Err("Sidecar returned an empty response line.".to_string());
        }

        Ok(trimmed)
    }
}

impl Drop for ManagedSidecar {
    fn drop(&mut self) {}
}

impl ManagedSpeechProcess {
    fn is_running(&self) -> bool {
        !self.poll_task.is_finished()
    }
}

struct SidecarState {
    process: Mutex<Option<ManagedSidecar>>,
    speech_process: Mutex<Option<ManagedSpeechProcess>>,
    root_override: Mutex<Option<PathBuf>>,
    config_path: PathBuf,
    resource_dir: Option<PathBuf>,
}

fn load_settings(path: &Path) -> AppSettings {
    fs::read_to_string(path)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default()
}

fn save_settings(path: &Path, settings: &AppSettings) -> Result<(), String> {
    let json = serde_json::to_string_pretty(settings)
        .map_err(|e| format!("Failed to serialize settings: {e}"))?;
    fs::write(path, json).map_err(|e| format!("Failed to save settings: {e}"))
}

fn auto_detect_root() -> Result<PathBuf, String> {
    let mut current =
        env::current_dir().map_err(|err| format!("Unable to read current dir: {err}"))?;
    loop {
        if current.join("Journal.Sidecar").exists() {
            return Ok(current);
        }
        if !current.pop() {
            return Err("Unable to locate repository root containing Journal.Sidecar.".to_string());
        }
    }
}

fn effective_root(root_override: &Option<PathBuf>) -> Result<PathBuf, String> {
    if let Some(root) = root_override {
        return Ok(root.clone());
    }
    auto_detect_root()
}

fn resolve_sidecar_path(root: &Path, resource_dir: Option<&Path>) -> Result<PathBuf, String> {
    #[cfg(windows)]
    let exe_name = "Journal.Sidecar.exe";
    #[cfg(not(windows))]
    let exe_name = "Journal.Sidecar";

    if root.is_file() && root.file_name().and_then(|n| n.to_str()) == Some(exe_name) {
        return Ok(root.to_path_buf());
    }

    let root_exe_path = root.join(exe_name);
    if root_exe_path.exists() {
        return Ok(root_exe_path);
    }

    let tauri_bin_sidecar_path = root.join("Journal.App").join("src-tauri").join("bin").join(exe_name);
    if tauri_bin_sidecar_path.exists() {
        return Ok(tauri_bin_sidecar_path);
    }

    let sidecar_src_root = root.join("Journal.Sidecar");
    if let Some(path) = find_sidecar_executable(&sidecar_src_root, exe_name) {
        return Ok(path);
    }

    if let Some(resource_dir) = resource_dir {
        let resource_sidecar_path = resource_dir.join("bin").join(exe_name);
        if resource_sidecar_path.exists() {
            return Ok(resource_sidecar_path);
        }
    }

    Err(format!(
        "{exe_name} not found in root, Journal.Sidecar tree, or resource dir for {}.",
        root.display()
    ))
}

fn parse_command_response(response_line: &str) -> Result<Value, String> {
    serde_json::from_str::<Value>(response_line)
        .map_err(|err| format!("Invalid sidecar JSON response: {err}"))
}

fn read_field<'a>(data: &'a Value, camel: &str, pascal: &str) -> Option<&'a Value> {
    data.get(camel).or_else(|| data.get(pascal))
}

fn find_sidecar_executable(search_root: &Path, exe_name: &str) -> Option<PathBuf> {
    if !search_root.is_dir() {
        return None;
    }

    let mut stack = vec![search_root.to_path_buf()];
    while let Some(dir) = stack.pop() {
        let Ok(entries) = fs::read_dir(&dir) else {
            continue;
        };

        for entry in entries {
            let Ok(entry) = entry else {
                continue;
            };
            let path = entry.path();
            if path.is_dir() {
                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                    if name == "node_modules" || name == ".git" || name == ".vs" {
                        continue;
                    }
                }
                stack.push(path);
                continue;
            }

            let is_sidecar_exe = path
                .file_name()
                .and_then(|name| name.to_str())
                .map(|name| name.eq_ignore_ascii_case(exe_name))
                .unwrap_or(false);
            if is_sidecar_exe {
                return Some(path);
            }
        }
    }

    None
}

async fn send_with_managed_sidecar(
    state: &SidecarState,
    input_line: &str,
) -> Result<String, String> {
    let root = {
        let root_override = state.root_override.lock().await;
        effective_root(&root_override)?
    };
    let mut guard = state.process.lock().await;

    for attempt in 1..=2 {
        let should_start = match guard.as_mut() {
            Some(existing) => !existing.is_running(),
            None => true,
        };
        if should_start {
            *guard = Some(ManagedSidecar::start(&root, state.resource_dir.as_deref())?);
        }

        let Some(process) = guard.as_mut() else {
            return Err("Sidecar process unavailable.".to_string());
        };

        match process.send_command_line(input_line).await {
            Ok(line) => return Ok(line),
            Err(err) => {
                *guard = None;
                if attempt == 2 {
                    return Err(err);
                }
            }
        }
    }

    Err("Failed to send command to sidecar.".to_string())
}

async fn send_sidecar_action(
    state: &SidecarState,
    action: &str,
    payload: Option<Value>,
) -> Result<Value, String> {
    let envelope = serde_json::json!({
        "action": action,
        "payload": payload.unwrap_or_else(|| serde_json::json!({}))
    });
    let input_line = serde_json::to_string(&envelope)
        .map_err(|err| format!("Serialize command failed: {err}"))?;
    let response_line = send_with_managed_sidecar(state, &input_line).await?;
    let response = parse_command_response(&response_line)?;

    let ok = response
        .get("ok")
        .and_then(|node| node.as_bool())
        .unwrap_or(false);
    if !ok {
        let err = response
            .get("error")
            .and_then(|node| node.as_str())
            .unwrap_or("Sidecar command failed.");
        return Err(err.to_string());
    }

    Ok(response
        .get("data")
        .cloned()
        .unwrap_or_else(|| serde_json::json!({})))
}

async fn stop_managed_sidecar(state: &SidecarState) {
    let mut guard = state.process.lock().await;
    guard.take();
}

async fn stop_speech_process(state: &SidecarState) -> Result<(), String> {
    let mut guard = state.speech_process.lock().await;
    if let Some(process) = guard.take() {
        process.poll_task.abort();
    }

    Ok(())
}

#[tauri::command]
async fn get_sidecar_root(state: tauri::State<'_, SidecarState>) -> Result<Value, String> {
    let root_override = state.root_override.lock().await.clone();
    let root = effective_root(&root_override)?;
    Ok(serde_json::json!({
        "root": root.to_string_lossy(),
        "isCustom": root_override.is_some()
    }))
}

#[tauri::command]
async fn set_sidecar_root(
    state: tauri::State<'_, SidecarState>,
    path: String,
) -> Result<Value, String> {
    let (new_override, root) = if path.trim().is_empty() {
        let detected = auto_detect_root()?;
        (None, detected)
    } else {
        let new_root = PathBuf::from(&path);
        if !new_root.exists() {
            return Err(format!(
                "Directory '{}' does not exist.",
                new_root.display()
            ));
        }
        resolve_sidecar_path(&new_root, state.resource_dir.as_deref())?;
        (Some(new_root.clone()), new_root)
    };

    // Stop the current sidecar so it restarts with new root
    {
        let mut guard = state.process.lock().await;
        guard.take();
    }

    let is_custom = new_override.is_some();
    *state.root_override.lock().await = new_override.clone();

    let mut settings = load_settings(&state.config_path);
    settings.sidecar_root = new_override.map(|p| p.to_string_lossy().into_owned());
    save_settings(&state.config_path, &settings)?;

    Ok(serde_json::json!({
        "root": root.to_string_lossy(),
        "isCustom": is_custom
    }))
}

#[tauri::command]
async fn get_ui_settings(state: tauri::State<'_, SidecarState>) -> Result<Value, String> {
    let settings = load_settings(&state.config_path);
    let tags = normalize_items(settings.tags, DEFAULT_SETTINGS_TAGS);
    let fragment_types = normalize_items(settings.fragment_types, DEFAULT_FRAGMENT_TYPES);
    let startup_view = normalize_startup_view(Some(settings.default_startup_view));

    Ok(serde_json::json!({
        "tags": tags,
        "fragmentTypes": fragment_types,
        "defaultStartupView": startup_view
    }))
}

#[tauri::command]
async fn set_ui_settings(
    state: tauri::State<'_, SidecarState>,
    tags: Vec<String>,
    fragment_types: Vec<String>,
    default_startup_view: Option<String>,
) -> Result<Value, String> {
    let mut settings = load_settings(&state.config_path);
    settings.tags = normalize_items(tags, DEFAULT_SETTINGS_TAGS);
    settings.fragment_types = normalize_items(fragment_types, DEFAULT_FRAGMENT_TYPES);
    settings.default_startup_view = normalize_startup_view(default_startup_view);
    save_settings(&state.config_path, &settings)?;

    Ok(serde_json::json!({
        "tags": settings.tags,
        "fragmentTypes": settings.fragment_types,
        "defaultStartupView": settings.default_startup_view
    }))
}

#[tauri::command]
async fn shutdown(
    state: tauri::State<'_, SidecarState>,
    app_handle: tauri::AppHandle,
) -> Result<(), String> {
    stop_speech_process(state.inner()).await?;
    let _ = send_sidecar_action(state.inner(), "speech.live.stop", None).await;
    stop_managed_sidecar(state.inner()).await;
    app_handle.exit(0);
    Ok(())
}

#[tauri::command]
async fn speech_start(
    state: tauri::State<'_, SidecarState>,
    app_handle: tauri::AppHandle,
) -> Result<Value, String> {
    let _ = app_handle.emit(
        "speech-status",
        serde_json::json!({ "state": "starting", "message": "Starting speech process..." }),
    );

    {
        let guard = state.speech_process.lock().await;
        if let Some(existing) = guard.as_ref() {
            if existing.is_running() {
                return Ok(serde_json::json!({ "running": true }));
            }
        }
    }

    let start_data = send_sidecar_action(state.inner(), "speech.live.start", None).await?;
    let running = read_field(&start_data, "running", "Running")
        .and_then(|node| node.as_bool())
        .unwrap_or(false);
    let status = read_field(&start_data, "status", "Status")
        .and_then(|node| node.as_str())
        .unwrap_or("starting");
    let warning = read_field(&start_data, "warning", "Warning")
        .and_then(|node| node.as_str())
        .map(|v| v.to_string());

    let _ = app_handle.emit(
        "speech-status",
        serde_json::json!({ "state": status, "message": warning.clone().unwrap_or_else(|| status.to_string()) }),
    );

    if !running {
        return Err(warning.unwrap_or_else(|| "Failed to start live speech.".to_string()));
    }

    let app_for_poll = app_handle.clone();
    let poll_task = tokio::spawn(async move {
        loop {
            let state_handle = app_for_poll.state::<SidecarState>();
            let poll_data = match send_sidecar_action(
                state_handle.inner(),
                "speech.live.poll",
                Some(serde_json::json!({ "maxItems": 8 })),
            )
            .await
            {
                Ok(value) => value,
                Err(err) => {
                    let _ = app_for_poll.emit(
                        "speech-status",
                        serde_json::json!({ "state": "error", "message": err }),
                    );
                    break;
                }
            };

            if let Some(items) = read_field(&poll_data, "items", "Items").and_then(|node| node.as_array()) {
                for item in items {
                    if let Some(text) = item.as_str() {
                        let _ = app_for_poll
                            .emit("speech-transcript", serde_json::json!({ "text": text }));
                    }
                }
            }

            let running = read_field(&poll_data, "running", "Running")
                .and_then(|node| node.as_bool())
                .unwrap_or(false);
            let status = read_field(&poll_data, "status", "Status")
                .and_then(|node| node.as_str())
                .unwrap_or(if running { "listening" } else { "stopped" });
            let warning = read_field(&poll_data, "warning", "Warning")
                .and_then(|node| node.as_str())
                .map(|v| v.to_string());
            if let Some(message) = warning {
                let _ = app_for_poll.emit(
                    "speech-status",
                    serde_json::json!({ "state": if running { "listening" } else { "error" }, "message": message }),
                );
            } else {
                let _ = app_for_poll.emit(
                    "speech-status",
                    serde_json::json!({ "state": status, "message": status }),
                );
            }

            if !running {
                break;
            }

            tokio::time::sleep(std::time::Duration::from_millis(350)).await;
        }
    });

    let mut guard = state.speech_process.lock().await;
    *guard = Some(ManagedSpeechProcess { poll_task });
    Ok(serde_json::json!({ "running": true }))
}

#[tauri::command]
async fn speech_stop(
    state: tauri::State<'_, SidecarState>,
    app_handle: tauri::AppHandle,
) -> Result<Value, String> {
    stop_speech_process(state.inner()).await?;
    let _ = send_sidecar_action(state.inner(), "speech.live.stop", None).await;
    let _ = app_handle.emit(
        "speech-status",
        serde_json::json!({ "state": "stopped", "message": "Dictation stopped." }),
    );
    Ok(serde_json::json!({ "running": false }))
}

#[tauri::command]
async fn speech_cleanup_probe(path: String) -> Result<Value, String> {
    if path.trim().is_empty() {
        return Ok(serde_json::json!({ "deleted": false }));
    }

    let target = PathBuf::from(path);
    let normalized = target.to_string_lossy().to_lowercase();
    if !normalized.contains("tauri-plugin-mic-recorder") || !normalized.ends_with(".wav") {
        return Ok(serde_json::json!({ "deleted": false }));
    }

    if !target.exists() {
        return Ok(serde_json::json!({ "deleted": false }));
    }

    fs::remove_file(&target).map_err(|err| format!("Failed to remove probe recording: {err}"))?;

    Ok(serde_json::json!({ "deleted": true }))
}

#[tauri::command]
async fn sidecar_command(
    state: tauri::State<'_, SidecarState>,
    command: CommandEnvelope,
) -> Result<Value, String> {
    if command.action.trim().is_empty() {
        return Err("Missing action".to_string());
    }

    let input_line = serde_json::to_string(&command)
        .map_err(|err| format!("Serialize command failed: {err}"))?;
    let response_line = send_with_managed_sidecar(state.inner(), &input_line).await?;
    parse_command_response(&response_line)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let app = tauri::Builder::default()
        .plugin(tauri_plugin_mic_recorder::init())
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![
            sidecar_command,
            shutdown,
            speech_start,
            speech_stop,
            speech_cleanup_probe,
            get_sidecar_root,
            set_sidecar_root,
            get_ui_settings,
            set_ui_settings,
        ])
        .setup(|app| {
            let config_dir = app.path().app_config_dir()?;
            fs::create_dir_all(&config_dir).ok();
            let config_path = config_dir.join("settings.json");
            let settings = load_settings(&config_path);
            let root_override = settings.sidecar_root.map(PathBuf::from);

            app.manage(SidecarState {
                process: Mutex::new(None),
                speech_process: Mutex::new(None),
                root_override: Mutex::new(root_override),
                config_path,
                resource_dir: app.path().resource_dir().ok(),
            });
            Ok(())
        })
        .build(tauri::generate_context!())
        .expect("error while building tauri application");

    app.run(|app_handle, event| {
        if let tauri::RunEvent::ExitRequested { .. } = event {
            let state = app_handle.state::<SidecarState>();
            if let Ok(mut guard) = state.process.try_lock() {
                guard.take();
            };
            if let Ok(mut guard) = state.speech_process.try_lock() {
                if let Some(speech) = guard.take() {
                    speech.poll_task.abort();
                }
            };
        }
    });
}
