Skip to content
use std::path::PathBuf;

use actix_web::{HttpRequest, HttpResponse, Responder, get, web};
use maud::{Markup, html};
use serde::Deserialize;

use crate::config::Config;
use crate::session::SessionStore;
use crate::view::export::resolve_server_url;
use crate::view::guard::guard_view;
use crate::view::icons;
use crate::view::{is_htmx, main_with_content};

const PLUGIN_MANIFEST: &str = include_str!("../../plugin/notify.desktop/manifest.json");
const PLUGIN_SERVICE: &str = include_str!("../../plugin/notify.desktop/Service.qml");
const PLUGIN_PANEL: &str = include_str!("../../plugin/notify.desktop/Panel.qml");
const PLUGIN_MODEL: &str = include_str!("../../plugin/notify.desktop/Model.js");
const PLUGIN_BAR_WIDGET: &str = include_str!("../../plugin/notify.desktop/BarWidget.qml");

#[derive(Debug, Clone, Deserialize, Default)]
pub struct DownloadQuery {
    pub url: Option<String>,
}

pub fn get_dist_dir() -> PathBuf {
    if let Ok(dir) = std::env::var("NOTIFY_DIST_DIR") {
        let p = PathBuf::from(dir.trim());
        if p.exists() {
            return p;
        }
    }
    for candidate in ["./dist", "/app/dist", "../dist"] {
        let p = PathBuf::from(candidate);
        if p.exists() {
            return p;
        }
    }
    PathBuf::from("./dist")
}

fn escape_bash(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('$', "\\$")
        .replace('`', "\\`")
}

pub fn url_encode(input: &str) -> String {
    let mut encoded = String::new();
    for b in input.bytes() {
        match b {
            b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                encoded.push(b as char);
            }
            _ => {
                encoded.push_str(&format!("%{:02X}", b));
            }
        }
    }
    encoded
}

pub fn build_mac_install_script(server_url: &str, pin: &str) -> String {
    let escaped_url = escape_bash(server_url);
    let escaped_pin = escape_bash(pin);

    format!(
        r#"#!/usr/bin/env bash
# Notify macOS App Installer
set -euo pipefail

if ! command -v curl >/dev/null 2>&1 || ! command -v unzip >/dev/null 2>&1; then
    echo "Error: curl and unzip are required to install Notify for macOS." >&2
    exit 1
fi

SERVER_URL="${{NOTIFY_SERVER_URL:-"{escaped_url}"}}"
PIN="${{NOTIFY_PIN:-"{escaped_pin}"}}"

echo "==> Installing Notify for macOS..."

TMP_DIR="$(mktemp -d)"
trap 'rm -rf "${{TMP_DIR}}"' EXIT

echo "==> Downloading Notify for macOS from ${{SERVER_URL}}..."
curl -fsSL -H "X-PIN: ${{PIN}}" "${{SERVER_URL}}/download/Notify-mac.zip?pin=${{PIN}}" -o "${{TMP_DIR}}/Notify-mac.zip"

echo "==> Extracting Notify.app..."
unzip -q -o "${{TMP_DIR}}/Notify-mac.zip" -d "${{TMP_DIR}}"

APP_DEST="/Applications/Notify.app"
if [ ! -w "/Applications" ]; then
    APP_DEST="${{HOME}}/Applications/Notify.app"
    mkdir -p "${{HOME}}/Applications"
fi

rm -rf "${{APP_DEST}}"
cp -R "${{TMP_DIR}}/Notify.app" "${{APP_DEST}}"

# Configure ~/.config/notify/notify.json
NOTIFY_CONFIG_DIR="${{HOME}}/.config/notify"
mkdir -p "${{NOTIFY_CONFIG_DIR}}"
curl -fsSL -H "X-PIN: ${{PIN}}" "${{SERVER_URL}}/install/omarchy/config?pin=${{PIN}}" -o "${{NOTIFY_CONFIG_DIR}}/notify.json"

echo "โœ“ Installed Notify.app to ${{APP_DEST}}"
echo "โœ“ Configured ${{NOTIFY_CONFIG_DIR}}/notify.json"
echo "๐ŸŽ‰ Launching Notify..."
open "${{APP_DEST}}" || true
"#
    )
}

pub fn build_linux_install_script(server_url: &str, pin: &str) -> String {
    let escaped_url = escape_bash(server_url);
    let escaped_pin = escape_bash(pin);

    format!(
        r#"#!/usr/bin/env bash
# Notify Linux desktop app installer
set -euo pipefail

if ! command -v curl >/dev/null 2>&1; then
    echo "Error: curl is required to install Notify for Linux." >&2
    exit 1
fi

SERVER_URL="${{NOTIFY_SERVER_URL:-"{escaped_url}"}}"
PIN="${{NOTIFY_PIN:-"{escaped_pin}"}}"
APP_DIR="${{XDG_DATA_HOME:-"${{HOME}}/.local/share"}}/notify"
BIN_DIR="${{HOME}}/.local/bin"
APPLICATIONS_DIR="${{XDG_DATA_HOME:-"${{HOME}}/.local/share"}}/applications"
TMP_FILE="$(mktemp)"
trap 'rm -f "${{TMP_FILE}}"' EXIT

mkdir -p "${{APP_DIR}}" "${{BIN_DIR}}" "${{APPLICATIONS_DIR}}"
echo "==> Downloading Notify for Linux from ${{SERVER_URL}}..."
curl -fsSL -H "X-PIN: ${{PIN}}" "${{SERVER_URL}}/download/Notify-linux-x86_64.AppImage" -o "${{TMP_FILE}}"
install -m 755 "${{TMP_FILE}}" "${{APP_DIR}}/Notify.AppImage"
ln -sfn "${{APP_DIR}}/Notify.AppImage" "${{BIN_DIR}}/notify"

cat > "${{APPLICATIONS_DIR}}/notify.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=Notify
Comment=Send quick notifications
Exec="${{APP_DIR}}/Notify.AppImage"
Terminal=false
Categories=Network;Utility;
EOF

NOTIFY_CONFIG_DIR="${{HOME}}/.config/notify"
mkdir -p "${{NOTIFY_CONFIG_DIR}}"
curl -fsSL -H "X-PIN: ${{PIN}}" "${{SERVER_URL}}/install/omarchy/config" -o "${{NOTIFY_CONFIG_DIR}}/notify.json"

echo "โœ“ Installed Notify to ${{APP_DIR}}/Notify.AppImage"
echo "โœ“ Added desktop entry to ${{APPLICATIONS_DIR}}/notify.desktop"
echo "โœ“ Configured ${{NOTIFY_CONFIG_DIR}}/notify.json"
echo "You can now launch Notify from your application menu or with: notify"
"#
    )
}

pub fn build_omarchy_install_script(server_url: &str, pin: &str) -> String {
    let escaped_url = escape_bash(server_url);
    let escaped_pin = escape_bash(pin);

    format!(
        r#"#!/usr/bin/env bash
# Notify plugin installer for Omarchy
set -euo pipefail

if ! command -v curl >/dev/null 2>&1; then
    echo "Error: curl is required to install the Notify plugin." >&2
    exit 1
fi

SERVER_URL="${{NOTIFY_SERVER_URL:-"{escaped_url}"}}"
PIN="${{NOTIFY_PIN:-"{escaped_pin}"}}"

echo "==> Installing Notify plugin for Omarchy..."

omarchy_plugins_dir="${{HOME}}/.config/omarchy/plugins"
plugin_target="${{omarchy_plugins_dir}}/notify.desktop"
notify_config_dir="${{HOME}}/.config/notify"

mkdir -p "${{plugin_target}}"
mkdir -p "${{notify_config_dir}}"

echo "==> Downloading plugin files from ${{SERVER_URL}}..."

FILES=(
    "manifest.json"
    "Service.qml"
    "Panel.qml"
    "Model.js"
    "BarWidget.qml"
)

for file in "${{FILES[@]}}"; do
    echo "  -> ${{file}}"
    curl -fsSL -H "X-PIN: ${{PIN}}" "${{SERVER_URL}}/install/omarchy/plugin/${{file}}?pin=${{PIN}}" -o "${{plugin_target}}/${{file}}"
done

echo "==> Setting up configuration..."
if [ -f "${{notify_config_dir}}/notify.json" ]; then
    bak="${{notify_config_dir}}/notify.json.bak.$(date +%s)"
    cp "${{notify_config_dir}}/notify.json" "${{bak}}"
    echo "  (Backed up existing config to ${{bak}})"
fi

curl -fsSL -H "X-PIN: ${{PIN}}" "${{SERVER_URL}}/install/omarchy/config?pin=${{PIN}}" -o "${{notify_config_dir}}/notify.json"
echo "โœ“ Installed configuration to ${{notify_config_dir}}/notify.json"

# Reload and enable plugin in Omarchy
if command -v omarchy-restart-shell >/dev/null 2>&1; then
    echo "==> Restarting Omarchy shell..."
    omarchy-restart-shell >/dev/null 2>&1 || true
elif command -v omarchy-shell >/dev/null 2>&1; then
    echo "==> Rescanning Omarchy shell plugins..."
    omarchy-shell shell rescanPlugins 2>/dev/null || true
fi

if command -v omarchy >/dev/null 2>&1; then
    echo "==> Enabling plugin in Omarchy..."
    omarchy plugin enable notify.desktop 2>/dev/null || true
fi

echo "๐ŸŽ‰ Notify plugin installed and configured successfully!"
"#
    )
}

pub fn install_content(server_url: &str, pin: &str) -> Markup {
    let pin_encoded = url_encode(pin);
    let omarchy_install_url = format!("{}/install/omarchy.sh?pin={}", server_url, pin_encoded);
    let omarchy_install_cmd = format!("curl -fsSL \"{}\" | bash", omarchy_install_url);

    let mac_install_url = format!("{}/install/mac.sh?pin={}", server_url, pin_encoded);
    let mac_install_cmd = format!("curl -fsSL \"{}\" | bash", mac_install_url);
    let mac_download_url = format!("{}/download/Notify-mac.zip?pin={}", server_url, pin_encoded);
    let linux_install_url = format!("{}/install/linux.sh?pin={}", server_url, pin_encoded);
    let linux_install_cmd = format!("curl -fsSL \"{}\" | bash", linux_install_url);
    let linux_download_url = format!(
        "{}/download/Notify-linux-x86_64.AppImage?pin={}",
        server_url, pin_encoded
    );
    let linux_deb_download_url = format!(
        "{}/download/Notify-linux-x86_64.deb?pin={}",
        server_url, pin_encoded
    );

    html! {
        div class="flex min-h-screen w-full flex-col" {
            nav class="navbar bg-base-100 shadow-sm border-b border-base-200 px-4" {
                div class="navbar-start" {
                    a
                        href="/"
                        class="btn btn-ghost text-xl"
                        hx-get="/notify"
                        hx-target="#main"
                        hx-swap="innerHTML"
                        hx-push-url="/" {
                        "Notify"
                    }
                }
                div class="navbar-end gap-2" {
                    a
                        href="/debug"
                        class="btn btn-ghost btn-sm"
                        hx-get="/debug"
                        hx-target="#main"
                        hx-swap="innerHTML"
                        hx-push-url="true" {
                        "Debug"
                    }
                    a
                        href="/"
                        class="btn btn-ghost btn-sm"
                        hx-get="/notify"
                        hx-target="#main"
                        hx-swap="innerHTML"
                        hx-push-url="/" {
                        "Back to notifications"
                    }
                }
            }

            div class="w-full flex-1 p-4 sm:p-8" {
                div class="mx-auto w-full max-w-5xl" {
                    div class="mb-8" {
                        p class="text-sm font-semibold uppercase tracking-widest text-base-content/60" {
                            "Integrations & Clients"
                        }
                        h1 class="text-3xl font-bold" { "Install Plugins & Apps" }
                        p class="mt-1 text-base-content/70" {
                            "Download and install pre-configured apps for Linux and macOS."
                        }
                    }

                    div class="flex flex-col gap-8" {
                        // Section 1: macOS desktop app
                        div class="card card-border bg-base-200 border-base-300 shadow-sm" {
                            div class="card-body p-6 sm:p-8" {
                                div class="flex flex-wrap items-start justify-between gap-4" {
                                    div class="flex items-center gap-3" {
                                        div class="flex size-12 items-center justify-center rounded-box bg-primary/10 text-primary" {
                                            (icons::apple())
                                        }
                                        div {
                                            h2 class="card-title text-2xl font-bold" { "macOS App" }
                                            p class="text-sm text-base-content/70" {
                                                "Native desktop app built with Tauri. Pre-configured for this server."
                                            }
                                        }
                                    }
                                    div {
                                        span class="badge badge-success badge-outline" { "Ready to install" }
                                    }
                                }

                                div class="divider my-2" {}

                                div class="grid grid-cols-1 md:grid-cols-2 gap-6" {
                                    div class="flex flex-col gap-4" {
                                        h3 class="font-semibold text-base" { "What's included" }
                                        ul class="space-y-2 text-sm text-base-content/80" {
                                            li class="flex items-start gap-2" {
                                                span class="text-success font-bold" { "โœ“" }
                                                span { "Native macOS application (universal Apple Silicon & Intel binary)" }
                                            }
                                            li class="flex items-start gap-2" {
                                                span class="text-success font-bold" { "โœ“" }
                                                span { "Trigger quick actions (heart, cat, emergency) and custom messages" }
                                            }
                                            li class="flex items-start gap-2" {
                                                span class="text-success font-bold" { "โœ“" }
                                                span { "Compatible with Omarchy profiles in ~/.config/notify" }
                                            }
                                            li class="flex items-start gap-2" {
                                                span class="text-success font-bold" { "โœ“" }
                                                span {
                                                    strong { "Pre-configured: " }
                                                    "Connects directly to "
                                                    code class="font-mono text-xs bg-base-300 px-1.5 py-0.5 rounded" { (server_url) }
                                                }
                                            }
                                        }

                                        div class="mt-2 flex flex-wrap gap-3" {
                                            a
                                                href=(mac_download_url)
                                                class="btn btn-primary btn-sm gap-2" {
                                                (icons::apple())
                                                "Download Notify.app (.zip)"
                                            }
                                            a
                                                href=(mac_install_url)
                                                target="_blank"
                                                class="btn btn-outline btn-sm gap-2" {
                                                (icons::terminal())
                                                "View macOS Installer Script"
                                            }
                                        }
                                    }

                                    div class="flex flex-col gap-3" {
                                        h3 class="font-semibold text-base" { "Quick Terminal Install" }
                                        p class="text-sm text-base-content/80" {
                                            "Run this one-line command to download, configure, and install into /Applications:"
                                        }

                                        div class="rounded-box bg-base-300 p-3 font-mono text-xs overflow-x-auto select-all" {
                                            (mac_install_cmd)
                                        }

                                        p class="text-xs text-base-content/60" {
                                            "Downloads the app, sets up "
                                            code class="font-mono text-xs bg-base-300 px-1 py-0.5 rounded" { "~/.config/notify/notify.json" }
                                            ", and launches Notify.app."
                                        }
                                    }
                                }
                            }
                        }

                        // Section 2: Linux desktop app
                        div class="card card-border bg-base-200 border-base-300 shadow-sm" {
                            div class="card-body p-6 sm:p-8" {
                                div class="flex flex-wrap items-start justify-between gap-4" {
                                    div class="flex items-center gap-3" {
                                        div class="flex size-12 items-center justify-center rounded-box bg-primary/10 text-primary" {
                                            (icons::terminal())
                                        }
                                        div {
                                            h2 class="card-title text-2xl font-bold" { "Linux App" }
                                            p class="text-sm text-base-content/70" {
                                                "Native Linux desktop app for x86_64, packaged as an AppImage."
                                            }
                                        }
                                    }
                                    span class="badge badge-success badge-outline" { "Ready to install" }
                                }

                                div class="divider my-2" {}

                                div class="grid grid-cols-1 md:grid-cols-2 gap-6" {
                                    div class="flex flex-col gap-4" {
                                        h3 class="font-semibold text-base" { "What's included" }
                                        ul class="space-y-2 text-sm text-base-content/80" {
                                            li class="flex items-start gap-2" {
                                                span class="text-success font-bold" { "โœ“" }
                                                span { "Linux AppImage and Debian package (DEB)" }
                                            }
                                            li class="flex items-start gap-2" {
                                                span class="text-success font-bold" { "โœ“" }
                                                span { "Quick actions, custom messages, and server profiles" }
                                            }
                                            li class="flex items-start gap-2" {
                                                span class="text-success font-bold" { "โœ“" }
                                                span { "AppImage requires WebKitGTK 4.1 and FUSE 2 at runtime" }
                                            }
                                        }
                                        div class="mt-2 flex flex-wrap gap-3" {
                                            a href=(linux_download_url) class="btn btn-primary btn-sm gap-2" {
                                                (icons::terminal())
                                                "Download Linux AppImage"
                                            }
                                            a href=(linux_deb_download_url) class="btn btn-outline btn-sm gap-2" {
                                                "Download Debian package"
                                            }
                                        }
                                    }

                                    div class="flex flex-col gap-3" {
                                        h3 class="font-semibold text-base" { "Quick Terminal Install" }
                                        p class="text-sm text-base-content/80" {
                                            "Install the app, add it to your application menu, and configure this server:"
                                        }
                                        div class="rounded-box bg-base-300 p-3 font-mono text-xs overflow-x-auto select-all" {
                                            (linux_install_cmd)
                                        }
                                        p class="text-xs text-base-content/60" {
                                            "The installer places the app in ~/.local/share/notify and creates a ~/.local/bin/notify command."
                                        }
                                    }
                                }
                            }
                        }

                        // Section 3: Omarchy Linux Plugin
                        div class="card card-border bg-base-200 border-base-300 shadow-sm" {
                            div class="card-body p-6 sm:p-8" {
                                div class="flex flex-wrap items-start justify-between gap-4" {
                                    div class="flex items-center gap-3" {
                                        div class="flex size-12 items-center justify-center rounded-box bg-primary/10 text-primary" {
                                            (icons::terminal())
                                        }
                                        div {
                                            h2 class="card-title text-2xl font-bold" { "Omarchy Plugin" }
                                            p class="text-sm text-base-content/70" {
                                                "Quickshell status bar widget and background notification daemon for Omarchy Linux."
                                            }
                                        }
                                    }
                                    div {
                                        span class="badge badge-success badge-outline" { "Ready to install" }
                                    }
                                }

                                div class="divider my-2" {}

                                div class="grid grid-cols-1 md:grid-cols-2 gap-6" {
                                    div class="flex flex-col gap-4" {
                                        h3 class="font-semibold text-base" { "What's included" }
                                        ul class="space-y-2 text-sm text-base-content/80" {
                                            li class="flex items-start gap-2" {
                                                span class="text-success font-bold" { "โœ“" }
                                                span { "Real-time background polling for incoming notifications" }
                                            }
                                            li class="flex items-start gap-2" {
                                                span class="text-success font-bold" { "โœ“" }
                                                span { "Status bar widget in the Omarchy top bar" }
                                            }
                                            li class="flex items-start gap-2" {
                                                span class="text-success font-bold" { "โœ“" }
                                                span { "Profile manager popup panel for multi-server setups" }
                                            }
                                            li class="flex items-start gap-2" {
                                                span class="text-success font-bold" { "โœ“" }
                                                span {
                                                    strong { "Direct configuration: " }
                                                    "Pre-configured to connect directly to "
                                                    code class="font-mono text-xs bg-base-300 px-1.5 py-0.5 rounded" { (server_url) }
                                                    " with your current PIN."
                                                }
                                            }
                                        }

                                        div class="mt-2 flex flex-wrap gap-3" {
                                            a
                                                href=(omarchy_install_url)
                                                target="_blank"
                                                class="btn btn-outline btn-sm gap-2" {
                                                (icons::terminal())
                                                "View Installer Script"
                                            }
                                        }
                                    }

                                    div class="flex flex-col gap-3" {
                                        h3 class="font-semibold text-base" { "Quick Install" }
                                        p class="text-sm text-base-content/80" {
                                            "Run this one-line command in your terminal to download and set up the plugin:"
                                        }

                                        div class="rounded-box bg-base-300 p-3 font-mono text-xs overflow-x-auto select-all" {
                                            (omarchy_install_cmd)
                                        }

                                        p class="text-xs text-base-content/60" {
                                            "The script automatically downloads all plugin files, writes "
                                            code class="font-mono text-xs bg-base-300 px-1 py-0.5 rounded" { "~/.config/notify/notify.json" }
                                            ", and enables the plugin in Omarchy."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

#[get("/install")]
pub async fn install_page(
    req: HttpRequest,
    session_store: web::Data<SessionStore>,
    config: web::Data<Config>,
) -> impl Responder {
    if !session_store.is_authenticated(&req) {
        let guard = guard_view(None);
        return if is_htmx(&req) {
            HttpResponse::Ok()
                .content_type("text/html; charset=utf-8")
                .body(guard.into_string())
        } else {
            HttpResponse::Ok()
                .content_type("text/html; charset=utf-8")
                .body(main_with_content(guard).into_string())
        };
    }

    let server_url = resolve_server_url(&req, None);
    let content = install_content(&server_url, &config.pin);

    if is_htmx(&req) {
        HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(content.into_string())
    } else {
        HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(main_with_content(content).into_string())
    }
}

pub async fn omarchy_installer_script(
    req: HttpRequest,
    session_store: web::Data<SessionStore>,
    query: web::Query<DownloadQuery>,
    config: web::Data<Config>,
) -> impl Responder {
    let is_session_auth = session_store.is_authenticated(&req);
    let is_pin_auth = crate::auth::extract_pin(&req)
        .map(|k| config.is_valid_pin(&k))
        .unwrap_or(false);

    if !is_session_auth && !is_pin_auth {
        return HttpResponse::Unauthorized()
            .content_type("text/plain; charset=utf-8")
            .body("Unauthorized: Invalid or missing PIN\n");
    }

    let server_url = resolve_server_url(&req, query.url.as_deref());
    let script = build_omarchy_install_script(&server_url, &config.pin);

    HttpResponse::Ok()
        .content_type("text/x-shellscript; charset=utf-8")
        .insert_header((
            actix_web::http::header::CACHE_CONTROL,
            "no-cache, no-store, must-revalidate",
        ))
        .body(script)
}

pub async fn mac_installer_script(
    req: HttpRequest,
    session_store: web::Data<SessionStore>,
    query: web::Query<DownloadQuery>,
    config: web::Data<Config>,
) -> impl Responder {
    let is_session_auth = session_store.is_authenticated(&req);
    let is_pin_auth = crate::auth::extract_pin(&req)
        .map(|k| config.is_valid_pin(&k))
        .unwrap_or(false);

    if !is_session_auth && !is_pin_auth {
        return HttpResponse::Unauthorized()
            .content_type("text/plain; charset=utf-8")
            .body("Unauthorized: Invalid or missing PIN\n");
    }

    let server_url = resolve_server_url(&req, query.url.as_deref());
    let script = build_mac_install_script(&server_url, &config.pin);

    HttpResponse::Ok()
        .content_type("text/x-shellscript; charset=utf-8")
        .insert_header((
            actix_web::http::header::CACHE_CONTROL,
            "no-cache, no-store, must-revalidate",
        ))
        .body(script)
}

pub async fn linux_installer_script(
    req: HttpRequest,
    session_store: web::Data<SessionStore>,
    query: web::Query<DownloadQuery>,
    config: web::Data<Config>,
) -> impl Responder {
    let is_session_auth = session_store.is_authenticated(&req);
    let is_pin_auth = crate::auth::extract_pin(&req)
        .map(|k| config.is_valid_pin(&k))
        .unwrap_or(false);

    if !is_session_auth && !is_pin_auth {
        return HttpResponse::Unauthorized()
            .content_type("text/plain; charset=utf-8")
            .body("Unauthorized: Invalid or missing PIN\n");
    }

    let server_url = resolve_server_url(&req, query.url.as_deref());
    let script = build_linux_install_script(&server_url, &config.pin);

    HttpResponse::Ok()
        .content_type("text/x-shellscript; charset=utf-8")
        .insert_header((
            actix_web::http::header::CACHE_CONTROL,
            "no-cache, no-store, must-revalidate",
        ))
        .body(script)
}

pub async fn download_file(
    req: HttpRequest,
    session_store: web::Data<SessionStore>,
    config: web::Data<Config>,
) -> impl Responder {
    let is_session_auth = session_store.is_authenticated(&req);
    let is_pin_auth = crate::auth::extract_pin(&req)
        .map(|k| config.is_valid_pin(&k))
        .unwrap_or(false);

    if !is_session_auth && !is_pin_auth {
        return HttpResponse::Unauthorized().finish();
    }

    let filename = req.match_info().query("filename");
    if filename.is_empty() || filename.contains("..") || filename.starts_with('/') {
        return HttpResponse::BadRequest().body("Invalid filename");
    }

    let dist_dir = get_dist_dir();
    let file_path = dist_dir.join(filename);

    if !file_path.exists() || !file_path.is_file() {
        return HttpResponse::NotFound()
            .body(format!("File '{filename}' not found in distribution"));
    }

    let bytes = match std::fs::read(&file_path) {
        Ok(b) => b,
        Err(e) => {
            log::error!("Failed to read distribution file {file_path:?}: {e}");
            return HttpResponse::InternalServerError().finish();
        }
    };

    let content_type = if filename.ends_with(".zip") {
        "application/zip"
    } else if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
        "application/gzip"
    } else if filename.ends_with(".json") {
        "application/json"
    } else if filename.ends_with(".sh") || filename.ends_with(".txt") || filename == "SHA256SUMS" {
        "text/plain; charset=utf-8"
    } else {
        "application/octet-stream"
    };

    HttpResponse::Ok()
        .content_type(content_type)
        .insert_header((
            actix_web::http::header::CONTENT_DISPOSITION,
            format!("attachment; filename=\"{filename}\""),
        ))
        .insert_header((
            actix_web::http::header::CACHE_CONTROL,
            "no-cache, no-store, must-revalidate",
        ))
        .body(bytes)
}

pub async fn omarchy_plugin_file(
    req: HttpRequest,
    session_store: web::Data<SessionStore>,
    config: web::Data<Config>,
) -> impl Responder {
    let is_session_auth = session_store.is_authenticated(&req);
    let is_pin_auth = crate::auth::extract_pin(&req)
        .map(|k| config.is_valid_pin(&k))
        .unwrap_or(false);

    if !is_session_auth && !is_pin_auth {
        return HttpResponse::Unauthorized().finish();
    }

    let filename = req.match_info().query("filename");
    match filename {
        "manifest.json" => HttpResponse::Ok()
            .content_type("application/json; charset=utf-8")
            .body(PLUGIN_MANIFEST),
        "Service.qml" => HttpResponse::Ok()
            .content_type("text/plain; charset=utf-8")
            .body(PLUGIN_SERVICE),
        "Panel.qml" => HttpResponse::Ok()
            .content_type("text/plain; charset=utf-8")
            .body(PLUGIN_PANEL),
        "Model.js" => HttpResponse::Ok()
            .content_type("application/javascript; charset=utf-8")
            .body(PLUGIN_MODEL),
        "BarWidget.qml" => HttpResponse::Ok()
            .content_type("text/plain; charset=utf-8")
            .body(PLUGIN_BAR_WIDGET),
        _ => HttpResponse::NotFound().body("Plugin file not found"),
    }
}

pub async fn omarchy_plugin_config(
    req: HttpRequest,
    session_store: web::Data<SessionStore>,
    query: web::Query<DownloadQuery>,
    config: web::Data<Config>,
) -> impl Responder {
    let is_session_auth = session_store.is_authenticated(&req);
    let is_pin_auth = crate::auth::extract_pin(&req)
        .map(|k| config.is_valid_pin(&k))
        .unwrap_or(false);

    if !is_session_auth && !is_pin_auth {
        return HttpResponse::Unauthorized().finish();
    }

    let server_url = resolve_server_url(&req, query.url.as_deref());
    let config_json = serde_json::json!({
        "name": "notify",
        "url": server_url,
        "pin": config.pin,
        "api_key": config.pin,
        "active": "notify"
    });

    let body = match serde_json::to_string_pretty(&config_json) {
        Ok(s) => s + "\n",
        Err(e) => {
            log::error!("Failed to serialize plugin config: {e}");
            return HttpResponse::InternalServerError().finish();
        }
    };

    HttpResponse::Ok()
        .content_type("application/json; charset=utf-8")
        .insert_header((
            actix_web::http::header::CACHE_CONTROL,
            "no-cache, no-store, must-revalidate",
        ))
        .body(body)
}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::http::header::HeaderValue;
    use actix_web::{App, cookie::Cookie, test as aw_test};

    fn test_config() -> Config {
        Config::new(
            8080,
            HeaderValue::from_static(crate::config::DEFAULT_CACHE_CONTROL),
            "notify.db".to_string(),
            false,
            "123456".to_string(),
        )
    }

    #[actix_web::test]
    async fn test_install_unauthenticated_returns_guard() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(SessionStore::in_memory());
        let app = aw_test::init_service(
            App::new()
                .app_data(config)
                .app_data(session_store)
                .service(install_page),
        )
        .await;

        let req = aw_test::TestRequest::get().uri("/install").to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert!(resp.status().is_success());

        let body = aw_test::read_body(resp).await;
        let body_str = String::from_utf8(body.to_vec()).unwrap();
        assert!(body_str.contains("class=\"otp\""));
        assert!(body_str.contains("Unlock"));
    }

    #[actix_web::test]
    async fn test_install_authenticated_returns_content() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(SessionStore::in_memory());
        let token = session_store.create_session().await;
        let cookie = Cookie::build(crate::session::SESSION_COOKIE_NAME, token).finish();

        let app = aw_test::init_service(
            App::new()
                .app_data(config)
                .app_data(session_store)
                .service(install_page),
        )
        .await;

        let req = aw_test::TestRequest::get()
            .uri("/install")
            .cookie(cookie)
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert!(resp.status().is_success());

        let body = aw_test::read_body(resp).await;
        let body_str = String::from_utf8(body.to_vec()).unwrap();

        // Sections
        assert!(body_str.contains("Omarchy Plugin"));
        assert!(body_str.contains("macOS App"));
        assert!(body_str.contains("Linux App"));
        assert!(body_str.contains("Ready to install"));
        assert!(body_str.contains("Notify.app"));

        // Script install command with pin
        assert!(body_str.contains("curl -fsSL"));
        assert!(body_str.contains("/install/omarchy.sh?pin=123456"));
        assert!(body_str.contains("/install/mac.sh?pin=123456"));
        assert!(body_str.contains("/install/linux.sh?pin=123456"));
        assert!(body_str.contains("Notify-linux-x86_64.AppImage?pin=123456"));
        assert!(body_str.contains("Notify-linux-x86_64.deb?pin=123456"));
        assert!(body_str.contains("View macOS Installer Script"));

        // Navigation
        assert!(body_str.contains("class=\"navbar"));
        assert!(body_str.contains("Back to notifications"));
        assert!(body_str.contains("Debug"));
        assert!(!body_str.contains("Export config"));
        assert!(!body_str.contains("download=\"notify.json\""));
        assert!(!body_str.contains("omarchy-notify-plugin.tar.gz"));
    }

    #[test]
    fn test_build_omarchy_install_script_contains_files_and_config() {
        let server_url = "http://192.168.1.100:9000";
        let pin = "secret_pin_42";

        let script = build_omarchy_install_script(server_url, pin);
        assert!(script.starts_with("#!/usr/bin/env bash"));
        assert!(
            script.contains("SERVER_URL=\"${NOTIFY_SERVER_URL:-\"http://192.168.1.100:9000\"}\"")
        );
        assert!(script.contains("PIN=\"${NOTIFY_PIN:-\"secret_pin_42\"}\""));

        // Plugin files downloaded
        assert!(script.contains("manifest.json"));
        assert!(script.contains("Service.qml"));
        assert!(script.contains("Panel.qml"));
        assert!(script.contains("Model.js"));
        assert!(script.contains("BarWidget.qml"));

        // Direct file download URL
        assert!(script.contains("/install/omarchy/plugin/${file}?pin=${PIN}"));

        // Config setup
        assert!(script.contains("/install/omarchy/config?pin=${PIN}"));
        assert!(script.contains("notify.json"));

        // Omarchy activation
        assert!(script.contains("omarchy-restart-shell"));
        assert!(script.contains("omarchy plugin enable notify.desktop"));
    }

    #[actix_web::test]
    async fn test_omarchy_installer_script_endpoint_unauthorized() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(SessionStore::in_memory());
        let app = aw_test::init_service(App::new().app_data(config).app_data(session_store).route(
            "/install/omarchy.sh",
            web::get().to(omarchy_installer_script),
        ))
        .await;

        let req = aw_test::TestRequest::get()
            .uri("/install/omarchy.sh")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::UNAUTHORIZED);
    }

    #[actix_web::test]
    async fn test_omarchy_installer_script_endpoint_authorized_with_pin() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(SessionStore::in_memory());
        let app = aw_test::init_service(App::new().app_data(config).app_data(session_store).route(
            "/install/omarchy.sh",
            web::get().to(omarchy_installer_script),
        ))
        .await;

        let req = aw_test::TestRequest::get()
            .uri("/install/omarchy.sh?pin=123456")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert!(resp.status().is_success());

        assert_eq!(
            resp.headers()
                .get("content-type")
                .unwrap()
                .to_str()
                .unwrap(),
            "text/x-shellscript; charset=utf-8"
        );

        let body = aw_test::read_body(resp).await;
        let body_str = String::from_utf8(body.to_vec()).unwrap();
        assert!(body_str.starts_with("#!/usr/bin/env bash"));
        assert!(body_str.contains("PIN=\"${NOTIFY_PIN:-\"123456\"}\""));
        assert!(body_str.contains("omarchy plugin enable notify.desktop"));
    }

    #[actix_web::test]
    async fn test_omarchy_plugin_file_endpoint() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(SessionStore::in_memory());
        let app = aw_test::init_service(App::new().app_data(config).app_data(session_store).route(
            "/install/omarchy/plugin/{filename:.*}",
            web::get().to(omarchy_plugin_file),
        ))
        .await;

        // Unauthorized request
        let req = aw_test::TestRequest::get()
            .uri("/install/omarchy/plugin/manifest.json")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::UNAUTHORIZED);

        // Authorized request for manifest.json
        let req = aw_test::TestRequest::get()
            .uri("/install/omarchy/plugin/manifest.json?pin=123456")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert!(resp.status().is_success());
        let body = aw_test::read_body(resp).await;
        let body_str = String::from_utf8(body.to_vec()).unwrap();
        assert!(body_str.contains("\"id\": \"notify.desktop\""));

        // Authorized request for unknown file
        let req = aw_test::TestRequest::get()
            .uri("/install/omarchy/plugin/unknown.txt?pin=123456")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::NOT_FOUND);
    }

    #[actix_web::test]
    async fn test_omarchy_plugin_config_endpoint() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(SessionStore::in_memory());
        let app = aw_test::init_service(App::new().app_data(config).app_data(session_store).route(
            "/install/omarchy/config",
            web::get().to(omarchy_plugin_config),
        ))
        .await;

        // Unauthorized request
        let req = aw_test::TestRequest::get()
            .uri("/install/omarchy/config")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::UNAUTHORIZED);

        // Authorized request
        let req = aw_test::TestRequest::get()
            .uri("/install/omarchy/config?pin=123456")
            .insert_header(("Host", "10.0.0.1:8080"))
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert!(resp.status().is_success());
        let body = aw_test::read_body(resp).await;
        let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(parsed["name"], "notify");
        assert_eq!(parsed["pin"], "123456");
        assert_eq!(parsed["api_key"], "123456");
        assert_eq!(parsed["url"], "http://10.0.0.1:8080");
        assert_eq!(parsed["active"], "notify");
    }

    #[test]
    fn test_build_mac_install_script_contains_download_and_config() {
        let script = build_mac_install_script("http://192.168.1.50:8080", "secretpin");
        assert!(script.starts_with("#!/usr/bin/env bash"));
        assert!(script.contains("Notify-mac.zip"));
        assert!(script.contains("Notify.app"));
        assert!(script.contains("http://192.168.1.50:8080"));
        assert!(script.contains("secretpin"));
        assert!(script.contains("/Applications/Notify.app"));
    }

    #[test]
    fn test_build_linux_install_script_contains_download_and_desktop_entry() {
        let script = build_linux_install_script("http://192.168.1.50:8080", "secretpin");
        assert!(script.starts_with("#!/usr/bin/env bash"));
        assert!(script.contains("Notify-linux-x86_64.AppImage"));
        assert!(script.contains("notify.desktop"));
        assert!(script.contains("http://192.168.1.50:8080"));
        assert!(script.contains("secretpin"));
        assert!(script.contains(r#"APP_DIR="${XDG_DATA_HOME:-"${HOME}/.local/share"}/notify""#));
        let syntax_check = std::process::Command::new("bash")
            .args(["-n", "-c", &script])
            .output()
            .expect("bash should be available to validate the Linux installer");
        assert!(
            syntax_check.status.success(),
            "Linux installer has invalid shell syntax: {}",
            String::from_utf8_lossy(&syntax_check.stderr)
        );
    }

    #[actix_web::test]
    async fn test_mac_installer_script_endpoint() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(SessionStore::in_memory());
        let app = aw_test::init_service(
            App::new()
                .app_data(config)
                .app_data(session_store)
                .route("/install/mac.sh", web::get().to(mac_installer_script)),
        )
        .await;

        // Unauth
        let req = aw_test::TestRequest::get()
            .uri("/install/mac.sh")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::UNAUTHORIZED);

        // Auth
        let req = aw_test::TestRequest::get()
            .uri("/install/mac.sh?pin=123456")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::OK);
        let body = aw_test::read_body(resp).await;
        let script = String::from_utf8(body.to_vec()).unwrap();
        assert!(script.contains("Notify-mac.zip"));
    }

    #[actix_web::test]
    async fn test_linux_installer_script_endpoint() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(SessionStore::in_memory());
        let app = aw_test::init_service(
            App::new()
                .app_data(config)
                .app_data(session_store)
                .route("/install/linux.sh", web::get().to(linux_installer_script)),
        )
        .await;

        let req = aw_test::TestRequest::get()
            .uri("/install/linux.sh")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::UNAUTHORIZED);

        let req = aw_test::TestRequest::get()
            .uri("/install/linux.sh?pin=123456")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::OK);
        let body = aw_test::read_body(resp).await;
        let script = String::from_utf8(body.to_vec()).unwrap();
        assert!(script.contains("Notify-linux-x86_64.AppImage"));
        assert!(script.contains("notify.desktop"));
    }

    #[actix_web::test]
    async fn test_download_file_endpoint() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(SessionStore::in_memory());
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let filename = format!("test-download-{}-{timestamp}.zip", std::process::id());
        let fixture_path = get_dist_dir().join(&filename);
        std::fs::write(&fixture_path, b"test download").unwrap();
        let app = aw_test::init_service(
            App::new()
                .app_data(config)
                .app_data(session_store)
                .route("/download/{filename:.*}", web::get().to(download_file)),
        )
        .await;

        // Unauth
        let req = aw_test::TestRequest::get()
            .uri("/download/Notify-mac.zip")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::UNAUTHORIZED);

        // Non-existent file
        let req = aw_test::TestRequest::get()
            .uri("/download/nonexistent.bin?pin=123456")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::NOT_FOUND);

        // Download a fixture archive.
        let req = aw_test::TestRequest::get()
            .uri(&format!("/download/{filename}?pin=123456"))
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::OK);
        assert_eq!(
            resp.headers()
                .get(actix_web::http::header::CONTENT_TYPE)
                .unwrap()
                .to_str()
                .unwrap(),
            "application/zip"
        );
        std::fs::remove_file(fixture_path).unwrap();
    }
}