//! resolveUser / resolveIdentity RPCs: bidirectional username ↔ identity key lookup.

use capnp::capability::Promise;
use quicproquo_proto::node_capnp::node_service;
use std::time::Duration;
use tokio::time::Instant;

use crate::auth::{check_rate_limit, coded_error, validate_auth_context};
use crate::error_codes::*;
use crate::metrics;
use crate::storage::StorageError;

use super::NodeServiceImpl;

/// Minimum response time for resolveUser to mask DB lookup timing differences.
const RESOLVE_TIMING_FLOOR: Duration = Duration::from_millis(5);

fn storage_err(err: StorageError) -> capnp::Error {
    coded_error(E009_STORAGE_ERROR, err)
}

impl NodeServiceImpl {
    pub fn handle_resolve_user(
        &mut self,
        params: node_service::ResolveUserParams,
        mut results: node_service::ResolveUserResults,
    ) -> Promise<(), capnp::Error> {
        let p = match params.get() {
            Ok(p) => p,
            Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
        };
        let username = match p.get_username() {
            Ok(u) => u,
            Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
        };
        let auth_ctx = match validate_auth_context(&self.auth_cfg, &self.sessions, p.get_auth()) {
            Ok(ctx) => ctx,
            Err(e) => return Promise::err(e),
        };

        let username_str = match username.to_str() {
            Ok(s) => s,
            Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
        };

        if username_str.is_empty() {
            return Promise::err(coded_error(E020_BAD_PARAMS, "username must not be empty"));
        }

        // Federation: parse user@domain format.
        let addr = crate::federation::address::FederatedAddress::parse(username_str);
        let is_remote = match (&addr.domain, &self.local_domain) {
            (Some(d), Some(ld)) => d != ld,
            (Some(_), None) => true,
            _ => false,
        };

        if is_remote {
            // Proxy to remote server via federation.
            if let (Some(ref fed_client), Some(ref domain)) = (&self.federation_client, &addr.domain) {
                if fed_client.has_peer(domain) {
                    let fed = fed_client.clone();
                    let user = addr.username.clone();
                    let dom = domain.clone();
                    return Promise::from_future(async move {
                        match fed.proxy_resolve_user(&dom, &user).await {
                            Ok(Some(key)) => {
                                results.get().set_identity_key(&key);
                            }
                            Ok(None) => {
                                // Not found on remote — return empty.
                            }
                            Err(e) => {
                                tracing::warn!(error = %e, "federation proxy_resolve_user failed");
                                // Fall through — return empty (not found).
                            }
                        }
                        Ok(())
                    });
                }
            }
            // No federation client or unknown peer — return empty (not found).
            return Promise::ok(());
        }

        // Rate-limit resolve requests to prevent bulk enumeration.
        if let Err(e) = check_rate_limit(&self.rate_limits, &auth_ctx.token) {
            tracing::warn!("rate_limit_hit");
            metrics::record_rate_limit_hit_total();
            return Promise::err(e);
        }

        // Timing floor: record the start time so we can pad the response to a
        // fixed minimum duration, masking DB-lookup timing differences between
        // existing and non-existing usernames.
        let deadline = Instant::now() + RESOLVE_TIMING_FLOOR;

        // Local resolution.
        let identity_key = match self.store.get_user_identity_key(&addr.username) {
            Ok(Some(key)) => key,
            Ok(None) => {
                // Return empty Data — caller checks length to detect "not found".
                // Pad to timing floor before responding.
                return Promise::from_future(async move {
                    tokio::time::sleep_until(deadline).await;
                    Ok(())
                });
            }
            Err(e) => return Promise::err(storage_err(e)),
        };

        let mut r = results.get();
        r.set_identity_key(&identity_key);

        // Attempt to include a KT Merkle inclusion proof.
        // Non-fatal: if the log is unavailable or has no entry, return just the key.
        if let Ok(log) = self.kt_log.lock() {
            if let Some(leaf_idx) = log.find(&addr.username, &identity_key) {
                match log.inclusion_proof(leaf_idx) {
                    Ok(proof) => match proof.to_bytes() {
                        Ok(bytes) => {
                            r.set_inclusion_proof(&bytes);
                        }
                        Err(e) => {
                            tracing::warn!(error = %e, "KT proof serialise failed");
                        }
                    },
                    Err(e) => {
                        tracing::warn!(error = %e, "KT inclusion_proof failed");
                    }
                }
            }
        }

        // Pad to timing floor before responding.
        Promise::from_future(async move {
            tokio::time::sleep_until(deadline).await;
            Ok(())
        })
    }

    pub fn handle_resolve_identity(
        &mut self,
        params: node_service::ResolveIdentityParams,
        mut results: node_service::ResolveIdentityResults,
    ) -> Promise<(), capnp::Error> {
        let p = match params.get() {
            Ok(p) => p,
            Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
        };
        let identity_key = match p.get_identity_key() {
            Ok(v) => v,
            Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)),
        };
        let _auth_ctx = match validate_auth_context(&self.auth_cfg, &self.sessions, p.get_auth()) {
            Ok(ctx) => ctx,
            Err(e) => return Promise::err(e),
        };

        if identity_key.len() != 32 {
            return Promise::err(coded_error(
                E004_IDENTITY_KEY_LENGTH,
                format!("identityKey must be exactly 32 bytes, got {}", identity_key.len()),
            ));
        }

        // Timing floor: mask DB-lookup timing differences (same as resolveUser).
        let deadline = Instant::now() + RESOLVE_TIMING_FLOOR;

        match self.store.resolve_identity_key(identity_key) {
            Ok(Some(username)) => {
                results.get().set_username(&username);
            }
            Ok(None) => {
                // Return empty string — caller checks length to detect "not found".
            }
            Err(e) => return Promise::err(storage_err(e)),
        }

        // Pad to timing floor before responding.
        Promise::from_future(async move {
            tokio::time::sleep_until(deadline).await;
            Ok(())
        })
    }
}
