//! Deploy page — generates a `CustomServer` blob the Windows / macOS / Linux
//! client accepts via `rustdesk --config <blob>`, plus the equivalent
//! rename-the-installer filename. The blob format is documented at
//! `<rustdesk>/src/custom_server.rs`: JSON → URL-safe-no-pad base64 →
//! reverse-the-string. The client tries the unsigned JSON path before
//! signature verification, so we don't need the Pro private key.

use super::shared::{html_escape, require_admin};
use crate::api::error::ApiError;
use crate::api::middleware::AuthedUser;
use axum::extract::Form;
use axum::http::HeaderMap;
use axum::response::Html;
use serde::Deserialize;
use serde_json::json;

pub async fn index(admin: AuthedUser, headers: HeaderMap) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    let pubkey = read_pubkey();
    // Best-effort prefill: the Host the admin's browser is currently
    // talking to is almost always the same machine running hbbs, so it's
    // the right default for the rendezvous-host field. Reverse proxies
    // forward the original Host through unless explicitly stripped, so
    // this works behind nginx/Caddy/Traefik too. Operator can edit if
    // hbbr lives on a different host.
    let host_default = headers
        .get(axum::http::header::HOST)
        .and_then(|v| v.to_str().ok())
        .map(host_only)
        .unwrap_or("")
        .to_string();
    let (api_default, relay_default) = if host_default.is_empty() {
        (String::new(), String::new())
    } else {
        (format!("https://{}", host_default), host_default.clone())
    };
    Ok(Html(render_form(
        &pubkey,
        &host_default,
        &api_default,
        &relay_default,
        "",
        None,
    )))
}

/// Strip an optional `:port` (and IPv6 brackets) from a Host-header value.
/// "rustdesk.example.com:21114"  -> "rustdesk.example.com"
/// "[::1]:21114"                 -> "::1"
/// "10.196.83.110"               -> "10.196.83.110"
fn host_only(s: &str) -> &str {
    if let Some(rest) = s.strip_prefix('[') {
        if let Some(end) = rest.find(']') {
            return &rest[..end];
        }
    }
    s.rsplit_once(':').map(|(h, _)| h).unwrap_or(s)
}

#[derive(Debug, Deserialize)]
pub struct DeployForm {
    #[serde(default)]
    pub host: String,
    #[serde(default)]
    pub api: String,
    #[serde(default)]
    pub relay: String,
    #[serde(default)]
    pub key: String,
}

pub async fn generate(
    admin: AuthedUser,
    Form(f): Form<DeployForm>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    if f.host.trim().is_empty() {
        return Ok(Html(render_form(
            &f.key,
            &f.host,
            &f.api,
            &f.relay,
            "",
            Some(("error", "Host is required.")),
        )));
    }
    let blob = encode_blob(&f.host, &f.key, &f.api, &f.relay);
    let result = render_result(&f.host, &f.key, &f.api, &f.relay, &blob);
    Ok(Html(render_form(
        &f.key,
        &f.host,
        &f.api,
        &f.relay,
        &result,
        None,
    )))
}

// ---------- helpers ----------

/// Best-effort read of the server's public key from `id_ed25519.pub` in CWD —
/// the same path `common::gen_sk` writes it to. If the file is missing
/// (operator passed `--key` explicitly, or the binary runs from a directory
/// they can't read), the field is left blank for them to paste.
fn read_pubkey() -> String {
    std::fs::read_to_string("id_ed25519.pub")
        .ok()
        .map(|s| s.trim().to_string())
        .unwrap_or_default()
}

/// Encode a `CustomServer` payload the way the client's
/// `get_custom_server_from_config_string` expects: JSON → URL-safe-no-pad
/// base64 → reverse the resulting string. The client reverses it back, base64
/// decodes, then JSON parses.
fn encode_blob(host: &str, key: &str, api: &str, relay: &str) -> String {
    let payload = json!({
        "host":  host,
        "key":   key,
        "api":   api,
        "relay": relay,
    });
    let b64 = base64::encode_config(payload.to_string().as_bytes(), base64::URL_SAFE_NO_PAD);
    b64.chars().rev().collect()
}

fn render_form(
    key: &str,
    host: &str,
    api: &str,
    relay: &str,
    result_html: &str,
    notice: Option<(&str, &str)>,
) -> String {
    let notice_html = match notice {
        Some((kind, msg)) => super::shared::notice_html(kind, msg),
        None => String::new(),
    };
    format!(
        r##"<div class="space-y-6">
  <header>
    <h2 class="text-lg font-semibold">Deploy</h2>
    <p class="text-xs text-slate-500 mt-1">Generate a config blob the stock client accepts via <code>rustdesk --config &lt;blob&gt;</code>, or the equivalent renamed-installer filename. The public key below is read from <code>id_ed25519.pub</code> on the server; override if you bootstrapped a custom keypair.</p>
  </header>

  {notice_html}

  <form
    class="space-y-3 bg-slate-900 border border-slate-800 rounded-lg p-4"
    hx-post="/admin/pages/deploy/generate"
    hx-target="#main"
    hx-swap="innerHTML"
  >
    <div>
      <label class="block text-xs font-medium text-slate-400 mb-1" for="host">Rendezvous host (required)</label>
      <input id="host" name="host" type="text" required value="{host}"
        placeholder="rustdesk.example.com or 203.0.113.10"
        oninput="
          const h = this.value.trim();
          const api = document.getElementById('api');
          const relay = document.getElementById('relay');
          if (api.dataset.derived !== '0') api.value = h ? 'https://' + h : '';
          if (relay.dataset.derived !== '0') relay.value = h;
          api.placeholder = h ? 'https://' + h : 'https://rustdesk.example.com';
        "
        class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm focus:outline-none focus:border-sky-500" />
      <p class="text-xs text-slate-500 mt-1">The hostname or IP clients reach hbbs at (TCP/UDP 21116).</p>
    </div>

    <div>
      <label class="block text-xs font-medium text-slate-400 mb-1" for="api">API URL (optional)</label>
      <input id="api" name="api" type="text" value="{api}"
        placeholder="https://rustdesk.example.com"
        oninput="this.dataset.derived = '0';"
        class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm focus:outline-none focus:border-sky-500" />
      <p class="text-xs text-slate-500 mt-1">Full URL of this admin/login API. Defaults to <code>https://&lt;host&gt;</code>; edit if your API runs on a different scheme/port. Leave blank to disable login on the client.</p>
    </div>

    <div>
      <label class="block text-xs font-medium text-slate-400 mb-1" for="relay">Relay host (optional)</label>
      <input id="relay" name="relay" type="text" value="{relay}"
        placeholder="rustdesk.example.com"
        oninput="this.dataset.derived = '0';"
        class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm focus:outline-none focus:border-sky-500" />
      <p class="text-xs text-slate-500 mt-1">Only set if hbbr runs on a separate host; otherwise leave blank.</p>
    </div>

    <div>
      <label class="block text-xs font-medium text-slate-400 mb-1" for="key">Public key</label>
      <textarea id="key" name="key" rows="2"
        class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-xs font-mono focus:outline-none focus:border-sky-500">{key}</textarea>
      <p class="text-xs text-slate-500 mt-1">Base64 contents of <code>id_ed25519.pub</code>.</p>
    </div>

    <button type="submit"
      class="bg-sky-600 hover:bg-sky-500 text-white text-sm font-medium rounded px-4 py-2 transition">
      Generate
    </button>
  </form>

  {result_html}
</div>"##,
        host = html_escape(host),
        api = html_escape(api),
        relay = html_escape(relay),
        key = html_escape(key),
        notice_html = notice_html,
        result_html = result_html,
    )
}

fn render_result(host: &str, key: &str, api: &str, relay: &str, blob: &str) -> String {
    // Build the rename-the-installer alternative. Windows filenames disallow
    // `:` and `/`, which the API URL is full of (`http://host:21114`). The
    // client falls back to `http://<host>:21114` when `api` is empty
    // (rustdesk/src/common.rs:get_api_server_), so we can omit the API field
    // from the filename whenever it matches that default. If the operator
    // supplied a non-default API URL we still build a "renamed" string for
    // reference but mark it as unusable on Windows and steer them to the
    // --config path.
    let default_api = format!("http://{}:21114", host);
    let api_is_default = api.is_empty() || api == default_api;
    let unsafe_chars = str_has_filename_unsafe_chars(api) && !api_is_default;

    let mut renamed = format!("rustdesk-host={}", host);
    if !key.is_empty() {
        renamed.push_str(&format!(",key={}", key));
    }
    if !api_is_default && !unsafe_chars {
        renamed.push_str(&format!(",api={}", api));
    }
    if !relay.is_empty() && !str_has_filename_unsafe_chars(relay) {
        renamed.push_str(&format!(",relay={}", relay));
    }
    renamed.push_str(".exe");

    let renamed_note = if unsafe_chars {
        r##"<p class="text-xs text-rose-300 mt-1">⚠ Your API URL contains <code>:</code> or <code>/</code>, which Windows forbids in filenames. The renamed-installer approach cannot carry it — use approach A above instead.</p>"##.to_string()
    } else if !api_is_default {
        format!(
            r##"<p class="text-xs text-amber-300 mt-1">⚠ Your API URL ({api_disp}) is not the default <code>http://&lt;host&gt;:21114</code>. The client will auto-derive the default from the rendezvous host on first launch, so this filename will deploy with the wrong API URL. Use approach A instead.</p>"##,
            api_disp = html_escape(api)
        )
    } else if !api.is_empty() {
        r##"<p class="text-xs text-slate-500 mt-1">API URL omitted from filename (Windows can't store <code>:</code> / <code>/</code>); the client auto-derives <code>http://&lt;host&gt;:21114</code> from the rendezvous host.</p>"##.to_string()
    } else {
        String::new()
    };

    let licensed = format!("rustdesk-licensed-{}", blob);
    let cmd_win = format!(
        r#""C:\Program Files\RustDesk\rustdesk.exe" --config {licensed}"#,
        licensed = licensed
    );
    let cmd_unix = format!("rustdesk --config {}", licensed);

    format!(
        r##"<section class="space-y-4 bg-slate-900 border border-slate-800 rounded-lg p-4">
  <header>
    <h3 class="text-sm font-semibold text-slate-200">Deployment artifact</h3>
    <p class="text-xs text-slate-500 mt-1">Pick whichever path fits your rollout. Both produce the same client config.</p>
  </header>

  <div>
    <label class="block text-xs font-medium text-slate-400 mb-1">A. Post-install command (Windows, Administrator)</label>
    <pre class="text-xs bg-slate-950 border border-slate-800 rounded p-2 overflow-x-auto select-all whitespace-pre-wrap break-all">{cmd_win}</pre>
    <p class="text-xs text-slate-500 mt-1">Requires the client to be installed and running as admin. Equivalent on macOS/Linux: <code class="text-slate-300">{cmd_unix}</code>.</p>
  </div>

  <div>
    <label class="block text-xs font-medium text-slate-400 mb-1">B. Renamed installer (drop-in)</label>
    <pre class="text-xs bg-slate-950 border border-slate-800 rounded p-2 overflow-x-auto select-all whitespace-pre-wrap break-all">{renamed}</pre>
    <p class="text-xs text-slate-500 mt-1">Rename the official RustDesk installer to this exact name and run it; the client reads its own filename on first launch and writes the config into the registry.</p>
    {renamed_note}
  </div>

  <details class="text-xs text-slate-400">
    <summary class="cursor-pointer text-slate-300 select-none">Raw blob</summary>
    <pre class="mt-2 bg-slate-950 border border-slate-800 rounded p-2 overflow-x-auto select-all whitespace-pre-wrap break-all">{blob}</pre>
  </details>
</section>"##,
        cmd_win = html_escape(&cmd_win),
        cmd_unix = html_escape(&cmd_unix),
        renamed = html_escape(&renamed),
        renamed_note = renamed_note,
        blob = html_escape(blob),
    )
}

/// Rough check for characters Windows disallows in filenames. We don't try
/// to be exhaustive (NUL, control chars etc. won't realistically appear in a
/// hostname or URL), just the ones a typical URL/relay value will trip on.
fn str_has_filename_unsafe_chars(s: &str) -> bool {
    s.chars()
        .any(|c| matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|'))
}
