// Per-boot unattended-access password.
//
// On every service start (= every host reboot, since `--service` is the
// Windows service entry the SCM auto-starts on boot) hello-agent generates
// a random "permanent password" and reports it to the rustdesk-server
// admin API. A supporter reaching the device when no user is logged in
// can read the password from the admin UI and authenticate without the
// per-session approval popup ever firing.
//
// The password is:
//   1. Persisted locally via `Config::set_permanent_password` so the
//      rustdesk auth path accepts it on the next LoginRequest.
//   2. POSTed to `<api-server>/api/unattended-password` with a retry
//      loop. The first few attempts can legitimately fail with
//      ID_NOT_FOUND because rendezvous registration runs in the
//      `--server` child (which the supervisor hasn't even spawned yet
//      when this fires), not in this `--service` process — we just back
//      off and retry until the peer row exists server-side.

use anyhow::{anyhow, Result};
use hbb_common::rand::{distributions::Alphanumeric, Rng};
use std::time::Duration;

const PASSWORD_LEN: usize = 12;
const MAX_RETRY_DELAY: Duration = Duration::from_secs(60);
const MAX_ATTEMPTS: u32 = 20;

/// Generate a fresh password, write it to local config, and kick off a
/// background reporter thread. Returns immediately; the reporter has its
/// own Tokio runtime so it doesn't tangle with the supervisor's poll loop.
pub fn rotate_and_report() {
    let password = generate();
    hbb_common::config::Config::set_permanent_password(&password);
    log::info!(
        "rotated unattended-access password (len={})",
        password.len()
    );

    std::thread::spawn(move || {
        let rt = match tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
        {
            Ok(rt) => rt,
            Err(e) => {
                log::warn!("unattended-password reporter: build runtime: {e}");
                return;
            }
        };
        rt.block_on(report_with_retry(password));
    });
}

fn generate() -> String {
    hbb_common::rand::thread_rng()
        .sample_iter(&Alphanumeric)
        .take(PASSWORD_LEN)
        .map(char::from)
        .collect()
}

async fn report_with_retry(password: String) {
    // Start at 2s and double up to MAX_RETRY_DELAY. The early ID_NOT_FOUND
    // window typically clears within a minute (heartbeat sync registers
    // the peer on its first iteration), so most boots land on the second
    // or third attempt. After MAX_ATTEMPTS we give up — the password is
    // already set locally, the only thing missing is its visibility in
    // the admin UI, so silent forever-retry would just be log spam.
    let mut delay = Duration::from_secs(2);
    for attempt in 1..=MAX_ATTEMPTS {
        match try_report(&password).await {
            Ok(_) => {
                log::info!(
                    "unattended-password: server acknowledged on attempt {attempt}"
                );
                return;
            }
            Err(e) => {
                log::warn!(
                    "unattended-password: report attempt {attempt}/{MAX_ATTEMPTS} \
                     failed ({e:#}); retrying in {:?}",
                    delay
                );
            }
        }
        tokio::time::sleep(delay).await;
        delay = (delay * 2).min(MAX_RETRY_DELAY);
    }
    log::error!(
        "unattended-password: gave up after {MAX_ATTEMPTS} attempts — admin UI \
         won't show the password until the next service start"
    );
}

async fn try_report(password: &str) -> Result<()> {
    let api = librustdesk::common::get_api_server(
        hbb_common::config::Config::get_option("api-server"),
        hbb_common::config::Config::get_option("custom-rendezvous-server"),
    );
    if api.is_empty() {
        return Err(anyhow!("no api-server configured yet"));
    }

    let url = format!("{api}/api/unattended-password");
    let id = hbb_common::config::Config::get_id();
    let uuid = librustdesk::common::encode64(hbb_common::get_uuid());
    let body = hbb_common::serde_json::json!({
        "id": id,
        "uuid": uuid,
        "password": password,
    })
    .to_string();

    // Same per-peer signature gate as heartbeat / sysinfo. Once this peer's
    // `managed` flag has flipped to 1 server-side, unsigned posts here
    // would be rejected — and we want unattended-password to keep landing
    // through the same TOFU lifecycle as the other endpoints.
    let headers = librustdesk::hbbs_http::sign::build_signed_headers(
        "POST",
        "/api/unattended-password",
        body.as_bytes(),
    )
    .unwrap_or_default();

    let resp = librustdesk::common::post_request(url, body, &headers)
        .await
        .map_err(|e| anyhow!("post: {e}"))?;
    let trimmed = resp.trim();
    if trimmed == "OK" {
        Ok(())
    } else {
        Err(anyhow!("unexpected response: {trimmed}"))
    }
}
