use std::{
    collections::VecDeque,
    sync::{Arc, Mutex},
    time::Duration,
};

use anyhow::Context;
use defguard_certs::{
    CertificateAuthority, CertificateInfo, CoreClientCert, Csr, PemLabel, der_to_pem,
};
use defguard_common::{
    VERSION,
    auth::claims::{Claims, ClaimsType},
    config::DefGuardConfig,
    db::models::{
        Certificates, WireguardNetwork,
        gateway::Gateway,
        proxy::Proxy,
        setup_auto_adoption::{
            AutoAdoptionComponentResult, AutoAdoptionWizardState, SetupAutoAdoptionComponent,
        },
        wireguard::{LocationMfaMode, ServiceLocationMode},
    },
    utils::strip_scheme,
};
use defguard_core::{
    setup_logs::scope_setup_logs,
    version::{MIN_GATEWAY_VERSION, MIN_PROXY_VERSION},
};
use defguard_proto::{
    common::{CertBundle, CertificateInfo as ProtoCertificateInfo},
    gateway::gateway_setup_client::GatewaySetupClient,
    proxy::proxy_setup_client::ProxySetupClient,
};
use defguard_version::{Version, client::ClientVersionInterceptor};
use ipnetwork::IpNetwork;
use reqwest::Url;
use sqlx::PgPool;
use tokio::{sync::mpsc::UnboundedReceiver, time::timeout};
use tonic::{
    Request, Status,
    service::Interceptor,
    transport::{Certificate, ClientTlsConfig, Endpoint},
};
use tracing::{debug, error, info, warn};

const TOKEN_CLIENT_ID: &str = "Defguard Core";
const STARTUP_ADOPTION_TIMEOUT: Duration = Duration::from_secs(10);
const AUTO_ADOPTION_CA_COMMON_NAME: &str = "Defguard Root CA";
const AUTO_ADOPTION_CA_EMAIL: &str = "root-ca@defguard.local";
const AUTO_ADOPTION_CA_VALIDITY_DAYS: u32 = 3650;
const GATEWAY_NAME: &str = "Gateway";
const PROXY_NAME: &str = "Edge";
const KEEPALIVE_INTERVAL_SECONDS: Duration = Duration::from_secs(5);

type SetupLogBuffer = Arc<Mutex<VecDeque<String>>>;

async fn ensure_ca_for_auto_adoption(pool: &PgPool) -> Result<(), anyhow::Error> {
    let mut certs = Certificates::get_or_default(pool)
        .await
        .context("Failed to load certificates")?;
    let has_cert = certs.ca_cert_der.is_some();
    let has_key = certs.ca_key_der.is_some();

    if has_cert && has_key {
        debug!("Auto-adoption mode: existing CA certificate/key found");
        return Ok(());
    }

    if has_cert && !has_key {
        warn!(
            "Auto-adoption mode requested but existing CA has no private key; generating new CA so startup adoption can proceed"
        );
    } else {
        info!("Auto-adoption mode requested with no CA configured; generating CA automatically");
    }

    let ca = CertificateAuthority::new(
        AUTO_ADOPTION_CA_COMMON_NAME,
        AUTO_ADOPTION_CA_EMAIL,
        AUTO_ADOPTION_CA_VALIDITY_DAYS,
    )
    .context("Failed to create automatic setup CA")?;

    certs.ca_cert_der = Some(ca.cert_der().to_vec());
    certs.ca_key_der = Some(ca.key_pair_der().to_vec());
    certs.ca_expiry = Some(
        ca.expiry()
            .context("Failed to determine automatic CA expiry")?,
    );

    certs
        .save(pool)
        .await
        .map_err(anyhow::Error::from)
        .context("Failed to persist automatically generated CA for auto-adoption")?;

    info!(
        "Automatic setup CA generated successfully for startup adoption mode (validity_days={AUTO_ADOPTION_CA_VALIDITY_DAYS})"
    );
    Ok(())
}

fn parse_host_port(input: &str) -> Result<(String, u16), anyhow::Error> {
    // Strip any scheme the user may have accidentally included (e.g. "http://host:port")
    let input = strip_scheme(input);

    if let Some(rest) = input.strip_prefix('[') {
        let (host, port_part) = rest
            .split_once(']')
            .context("Invalid endpoint format. Expected [ipv6]:port")?;
        let port = port_part
            .strip_prefix(':')
            .context("Invalid endpoint format. Missing port separator ':'")?
            .parse::<u16>()
            .context("Invalid port in endpoint")?;
        return Ok((host.to_string(), port));
    }

    let (host, port) = input
        .rsplit_once(':')
        .context("Invalid endpoint format. Expected host:port")?;
    if host.trim().is_empty() {
        anyhow::bail!("Invalid endpoint format. Host cannot be empty");
    }

    Ok((
        host.to_string(),
        port.parse::<u16>().context("Invalid port in endpoint")?,
    ))
}

#[derive(Clone)]
struct AuthInterceptor {
    token: String,
}

impl AuthInterceptor {
    const fn new(token: String) -> Self {
        Self { token }
    }
}

impl Interceptor for AuthInterceptor {
    fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
        request.metadata_mut().insert(
            "authorization",
            format!("Bearer {}", self.token)
                .parse()
                .expect("failed to parse auth metadata"),
        );
        Ok(request)
    }
}

struct TaskGuard(tokio::task::JoinHandle<()>);

impl Drop for TaskGuard {
    fn drop(&mut self) {
        self.0.abort();
    }
}

fn format_component_log(timestamp: &str, level: &str, target: &str, message: &str) -> String {
    let level = level
        .strip_prefix("Level(")
        .and_then(|value| value.strip_suffix(')'))
        .unwrap_or(level)
        .to_uppercase();

    format!("{timestamp} {level} {target}: message={message}")
}

fn collect_stream_logs(log_rx: &mut UnboundedReceiver<String>) -> Vec<String> {
    let mut logs = Vec::new();
    while let Ok(log) = log_rx.try_recv() {
        logs.push(log);
    }
    logs
}

fn collect_core_logs(log_buffer: &SetupLogBuffer) -> Vec<String> {
    let mut guard = log_buffer
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    std::mem::take(&mut *guard).into_iter().collect()
}

fn merge_failure_logs(
    message: impl Into<String>,
    log_buffer: &SetupLogBuffer,
    log_rx: &mut UnboundedReceiver<String>,
) -> (bool, Vec<String>, Option<ComponentAdoptionResult>) {
    let msg = message.into();
    error!("{msg}");
    let mut logs = collect_core_logs(log_buffer);
    logs.extend(collect_stream_logs(log_rx));
    (false, logs, None)
}

fn logs_to_persist(success: bool, logs: Vec<String>) -> Vec<String> {
    if success { Vec::new() } else { logs }
}

/// Carries the result of a successful component adoption attempt.
///
/// Bundles the parsed certificate metadata with the Core gRPC client
/// certificate materials so that both can be persisted in the same DB
/// transaction without re-issuing the client cert.
struct ComponentAdoptionResult {
    cert_info: CertificateInfo,
    core_client: CoreClientCert,
}

async fn run_edge_adoption_attempt(
    pool: &PgPool,
    host: &str,
    port: u16,
) -> (bool, Vec<String>, Option<ComponentAdoptionResult>) {
    let log_buffer = Arc::new(Mutex::new(VecDeque::new()));
    let certs = match Certificates::get_or_default(pool).await {
        Ok(c) => c,
        Err(err) => {
            let msg = format!("Failed to load certificates: {err}");
            error!("{msg}");
            return (false, vec![msg], None);
        }
    };
    let Some(ca_cert_der) = certs.ca_cert_der else {
        let msg = "CA certificate not found in settings".to_string();
        error!("{msg}");
        return (false, vec![msg], None);
    };
    let Some(ca_key_der) = certs.ca_key_der else {
        let msg = "CA private key not found in settings. Uploading CA cert without key cannot auto-adopt.".to_string();
        error!("{msg}");
        return (false, vec![msg], None);
    };
    scope_setup_logs(Arc::clone(&log_buffer), async move {
        run_edge_adoption_attempt_scoped(host, port, log_buffer, ca_cert_der, ca_key_der).await
    })
    .await
}

async fn run_edge_adoption_attempt_scoped(
    host: &str,
    port: u16,
    log_buffer: SetupLogBuffer,
    ca_cert_der: Vec<u8>,
    ca_key_der: Vec<u8>,
) -> (bool, Vec<String>, Option<ComponentAdoptionResult>) {
    debug!("Starting edge adoption attempt host={host} port={port}");
    let (log_tx, mut log_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
    let endpoint_str = format!("http://{host}:{port}");
    let url = match Url::parse(&endpoint_str) {
        Ok(url) => url,
        Err(err) => {
            return merge_failure_logs(
                format!("Invalid edge endpoint URL: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("Successfully validated Edge address: {endpoint_str}");

    let cert_pem = match der_to_pem(&ca_cert_der, PemLabel::Certificate) {
        Ok(pem) => pem,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to convert CA certificate to PEM: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("Loaded CA certificate for secure Edge communication");

    let base_endpoint = match Endpoint::from_shared(endpoint_str.clone()) {
        Ok(endpoint) => endpoint,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to build edge endpoint: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };

    let base_endpoint = base_endpoint
        .http2_keep_alive_interval(KEEPALIVE_INTERVAL_SECONDS)
        .tcp_keepalive(Some(KEEPALIVE_INTERVAL_SECONDS))
        .keep_alive_while_idle(true);

    let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(cert_pem));
    let endpoint = match base_endpoint.tls_config(tls) {
        Ok(endpoint) => endpoint,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to configure TLS for edge endpoint: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("Prepared secure connection endpoint for Edge at {host}:{port}");

    let core_version = match Version::parse(VERSION) {
        Ok(version) => version,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to parse core version: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("Parsed Core version {core_version} for Edge auto-adoption");

    let token = match Claims::new(
        ClaimsType::Gateway,
        url.to_string(),
        TOKEN_CLIENT_ID.to_string(),
        u32::MAX.into(),
    )
    .to_jwt()
    {
        Ok(token) => token,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to generate setup token: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("Generated secure setup token for Edge authentication");

    let version_interceptor = ClientVersionInterceptor::new(core_version.clone());
    let auth_interceptor = AuthInterceptor::new(token);

    let mut client =
        ProxySetupClient::with_interceptor(endpoint.connect_lazy(), move |mut req: Request<()>| {
            req = version_interceptor.clone().call(req)?;
            auth_interceptor.clone().call(req)
        });

    let response_with_metadata = match timeout(STARTUP_ADOPTION_TIMEOUT, client.start(())).await {
        Ok(Ok(response)) => response,
        Ok(Err(err)) => {
            return merge_failure_logs(
                format!("Failed to start edge setup stream: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
        Err(_) => {
            return merge_failure_logs(
                format!(
                    "Timed out connecting to edge setup endpoint after {} seconds",
                    STARTUP_ADOPTION_TIMEOUT.as_secs()
                ),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("Successfully connected to Edge setup stream");

    let edge_version = response_with_metadata
        .metadata()
        .get(defguard_version::VERSION_HEADER)
        .and_then(|v| v.to_str().ok())
        .map(defguard_version::Version::parse)
        .transpose()
        .unwrap_or(None);

    if let Some(edge_version) = edge_version {
        if edge_version < MIN_PROXY_VERSION {
            return merge_failure_logs(
                format!(
                    "Edge version {edge_version} is below minimum required {MIN_PROXY_VERSION}; aborting adoption"
                ),
                &log_buffer,
                &mut log_rx,
            );
        }
        debug!("Edge version {edge_version} accepted; proceeding with CSR exchange");
    } else {
        return merge_failure_logs(
            "Edge component did not return a version header; cannot verify compatibility",
            &log_buffer,
            &mut log_rx,
        );
    }

    let mut response = response_with_metadata.into_inner();
    let spawn_log_buffer = Arc::clone(&log_buffer);
    let log_reader_task = tokio::spawn(scope_setup_logs(spawn_log_buffer, async move {
        loop {
            match response.message().await {
                Ok(Some(entry)) => {
                    let formatted = format_component_log(
                        &entry.timestamp,
                        &entry.level,
                        &entry.target,
                        &entry.message,
                    );
                    if log_tx.send(formatted).is_err() {
                        break;
                    }
                }
                Ok(None) => break,
                Err(err) => {
                    let _ = log_tx.send(format!("Error reading log: {err}"));
                    break;
                }
            }
        }
    }));
    let _log_task_guard = TaskGuard(log_reader_task);

    let Some(hostname) = url.host_str() else {
        return merge_failure_logs(
            format!("Failed to extract hostname from edge/proxy URL: {url}"),
            &log_buffer,
            &mut log_rx,
        );
    };

    debug!("Requesting CSR from proxy hostname={hostname}");
    let csr_response = match client
        .get_csr(ProtoCertificateInfo {
            cert_hostname: hostname.to_string(),
        })
        .await
    {
        Ok(response) => response.into_inner(),
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to get CSR from proxy: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("CSR received from proxy hostname={hostname}");

    let csr = match Csr::from_der(&csr_response.der_data) {
        Ok(csr) => csr,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to parse CSR from proxy: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };

    let ca = match CertificateAuthority::from_cert_der_key_pair(&ca_cert_der, &ca_key_der) {
        Ok(ca) => ca,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to build certificate authority: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };

    let cert = match ca.sign_server_cert(&csr) {
        Ok(cert) => cert,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to sign CSR for proxy: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("CSR signed for proxy hostname={hostname}; sending certificate");

    let core_client = match ca.issue_core_client_cert(hostname) {
        Ok(c) => c,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to issue Core client certificate for proxy: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };

    let bundle = CertBundle {
        component_cert_der: cert.der().to_vec(),
        ca_cert_der: ca_cert_der.clone(),
        core_client_cert_der: core_client.cert_der.clone(),
    };
    if let Err(err) = client.send_cert(bundle).await {
        return merge_failure_logs(
            format!("Failed to send certificate to proxy: {err}"),
            &log_buffer,
            &mut log_rx,
        );
    }
    debug!("Certificate delivered to proxy hostname={hostname}");

    let cert_info = match CertificateInfo::from_der(cert.der()) {
        Ok(info) => info,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to parse certificate info from proxy cert: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!(
        "Edge adoption handshake complete hostname={hostname} cert_serial={} expires={}",
        cert_info.serial, cert_info.not_after
    );

    let mut logs = collect_stream_logs(&mut log_rx);
    if logs.is_empty() {
        logs = vec!["No runtime logs received from edge component".to_string()];
    }

    (
        true,
        logs,
        Some(ComponentAdoptionResult {
            cert_info,
            core_client,
        }),
    )
}

async fn run_gateway_adoption_attempt(
    pool: &PgPool,
    host: &str,
    port: u16,
) -> (bool, Vec<String>, Option<ComponentAdoptionResult>) {
    let log_buffer = Arc::new(Mutex::new(VecDeque::new()));
    let certs = match Certificates::get_or_default(pool).await {
        Ok(c) => c,
        Err(err) => {
            let msg = format!("Failed to load certificates: {err}");
            error!("{msg}");
            return (false, vec![msg], None);
        }
    };
    let Some(ca_cert_der) = certs.ca_cert_der else {
        let msg = "CA certificate not found in settings".to_string();
        error!("{msg}");
        return (false, vec![msg], None);
    };
    let Some(ca_key_der) = certs.ca_key_der else {
        let msg = "CA private key not found in settings. Uploading CA cert without key cannot auto-adopt.".to_string();
        error!("{msg}");
        return (false, vec![msg], None);
    };
    scope_setup_logs(Arc::clone(&log_buffer), async move {
        run_gateway_adoption_attempt_scoped(host, port, log_buffer, ca_cert_der, ca_key_der).await
    })
    .await
}

async fn run_gateway_adoption_attempt_scoped(
    host: &str,
    port: u16,
    log_buffer: SetupLogBuffer,
    ca_cert_der: Vec<u8>,
    ca_key_der: Vec<u8>,
) -> (bool, Vec<String>, Option<ComponentAdoptionResult>) {
    debug!("Starting gateway adoption attempt host={host} port={port}");
    let (log_tx, mut log_rx) = tokio::sync::mpsc::unbounded_channel::<String>();

    let endpoint_str = format!("http://{host}:{port}");
    let url = match Url::parse(&endpoint_str) {
        Ok(url) => url,
        Err(err) => {
            return merge_failure_logs(
                format!("Invalid gateway endpoint URL: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("Successfully validated Gateway address: {endpoint_str}");

    let cert_pem = match der_to_pem(&ca_cert_der, PemLabel::Certificate) {
        Ok(pem) => pem,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to convert CA certificate to PEM: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("Loaded CA certificate for secure Gateway communication");

    let base_endpoint = match Endpoint::from_shared(endpoint_str.clone()) {
        Ok(endpoint) => endpoint,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to build gateway endpoint: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };

    let base_endpoint = base_endpoint
        .http2_keep_alive_interval(KEEPALIVE_INTERVAL_SECONDS)
        .tcp_keepalive(Some(KEEPALIVE_INTERVAL_SECONDS))
        .keep_alive_while_idle(true);

    let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(cert_pem));
    let endpoint = match base_endpoint.tls_config(tls) {
        Ok(endpoint) => endpoint,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to configure TLS for gateway endpoint: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("Prepared secure connection endpoint for Gateway at {host}:{port}");

    let core_version = match Version::parse(VERSION) {
        Ok(version) => version,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to parse core version: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("Parsed Core version {core_version} for Gateway auto-adoption");

    let token = match Claims::new(
        ClaimsType::Gateway,
        url.to_string(),
        TOKEN_CLIENT_ID.to_string(),
        u32::MAX.into(),
    )
    .to_jwt()
    {
        Ok(token) => token,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to generate setup token: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("Generated secure setup token for Gateway authentication");

    let version_interceptor = ClientVersionInterceptor::new(core_version.clone());
    let auth_interceptor = AuthInterceptor::new(token);

    let mut client = GatewaySetupClient::with_interceptor(
        endpoint.connect_lazy(),
        move |mut req: Request<()>| {
            req = version_interceptor.clone().call(req)?;
            auth_interceptor.clone().call(req)
        },
    );

    let response_with_metadata = match timeout(STARTUP_ADOPTION_TIMEOUT, client.start(())).await {
        Ok(Ok(response)) => response,
        Ok(Err(err)) => {
            return merge_failure_logs(
                format!("Failed to start gateway setup stream: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
        Err(_) => {
            return merge_failure_logs(
                format!(
                    "Timed out connecting to gateway setup endpoint after {} seconds",
                    STARTUP_ADOPTION_TIMEOUT.as_secs()
                ),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("Successfully connected to Gateway setup stream");

    let gateway_version = response_with_metadata
        .metadata()
        .get(defguard_version::VERSION_HEADER)
        .and_then(|v| v.to_str().ok())
        .map(defguard_version::Version::parse)
        .transpose()
        .unwrap_or(None);

    if let Some(gateway_version) = gateway_version {
        if gateway_version < MIN_GATEWAY_VERSION {
            return merge_failure_logs(
                format!(
                    "Gateway version {gateway_version} is below minimum required {MIN_GATEWAY_VERSION}; aborting adoption"
                ),
                &log_buffer,
                &mut log_rx,
            );
        }
        debug!("Gateway version {gateway_version} accepted; proceeding with CSR exchange");
    } else {
        return merge_failure_logs(
            "Gateway component did not return a version header; cannot verify compatibility",
            &log_buffer,
            &mut log_rx,
        );
    }

    let mut response = response_with_metadata.into_inner();
    let spawn_log_buffer = Arc::clone(&log_buffer);
    let log_reader_task = tokio::spawn(scope_setup_logs(spawn_log_buffer, async move {
        loop {
            match response.message().await {
                Ok(Some(entry)) => {
                    let formatted = format_component_log(
                        &entry.timestamp,
                        &entry.level,
                        &entry.target,
                        &entry.message,
                    );
                    if log_tx.send(formatted).is_err() {
                        break;
                    }
                }
                Ok(None) => break,
                Err(err) => {
                    let _ = log_tx.send(format!("Error reading log: {err}"));
                    break;
                }
            }
        }
    }));
    let _log_task_guard = TaskGuard(log_reader_task);

    let Some(hostname) = url.host_str() else {
        return merge_failure_logs(
            format!("Failed to extract hostname from gateway URL: {url}"),
            &log_buffer,
            &mut log_rx,
        );
    };

    debug!("Requesting CSR from gateway hostname={hostname}");
    let csr_response = match client
        .get_csr(ProtoCertificateInfo {
            cert_hostname: hostname.to_string(),
        })
        .await
    {
        Ok(response) => response.into_inner(),
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to get CSR from gateway: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("CSR received from gateway hostname={hostname}");

    let csr = match Csr::from_der(&csr_response.der_data) {
        Ok(csr) => csr,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to parse CSR from gateway: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };

    let ca = match CertificateAuthority::from_cert_der_key_pair(&ca_cert_der, &ca_key_der) {
        Ok(ca) => ca,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to build certificate authority: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };

    let cert = match ca.sign_server_cert(&csr) {
        Ok(cert) => cert,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to sign CSR for gateway: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!("CSR signed for gateway hostname={hostname}; sending certificate");

    let core_client = match ca.issue_core_client_cert(hostname) {
        Ok(c) => c,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to issue Core client certificate for gateway: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };

    let bundle = CertBundle {
        component_cert_der: cert.der().to_vec(),
        ca_cert_der: ca_cert_der.clone(),
        core_client_cert_der: core_client.cert_der.clone(),
    };
    if let Err(err) = client.send_cert(bundle).await {
        return merge_failure_logs(
            format!("Failed to send certificate to gateway: {err}"),
            &log_buffer,
            &mut log_rx,
        );
    }
    debug!("Certificate delivered to gateway hostname={hostname}");

    let cert_info = match CertificateInfo::from_der(cert.der()) {
        Ok(info) => info,
        Err(err) => {
            return merge_failure_logs(
                format!("Failed to parse certificate info from gateway cert: {err}"),
                &log_buffer,
                &mut log_rx,
            );
        }
    };
    debug!(
        "Gateway adoption handshake complete hostname={hostname} cert_serial={} expires={}",
        cert_info.serial, cert_info.not_after
    );

    let mut logs = collect_stream_logs(&mut log_rx);
    if logs.is_empty() {
        logs = vec!["No runtime logs received from gateway component".to_string()];
    }

    (
        true,
        logs,
        Some(ComponentAdoptionResult {
            cert_info,
            core_client,
        }),
    )
}

// Default WireGuard network address and port used when auto-adopting a gateway without an
// existing network.  The gateway's own gRPC host is reused as the WireGuard endpoint so peers
// can reach it.
const DEFAULT_AUTO_ADOPTION_NETWORK_ADDRESS: &str = "10.0.0.1/24";
const DEFAULT_AUTO_ADOPTION_WIREGUARD_PORT: i32 = 51820;

async fn process_startup_auto_adoption(
    pool: &PgPool,
    component: SetupAutoAdoptionComponent,
    endpoint: &str,
) -> Result<(), anyhow::Error> {
    let (host, port) = parse_host_port(endpoint)?;

    let (status, logs, cert_info) = match component {
        SetupAutoAdoptionComponent::Edge => run_edge_adoption_attempt(pool, &host, port).await,
        SetupAutoAdoptionComponent::Gateway => {
            run_gateway_adoption_attempt(pool, &host, port).await
        }
    };

    // On successful adoption: create the relevant DB records.
    if status {
        match component {
            SetupAutoAdoptionComponent::Gateway => {
                if let Some(result) = cert_info
                    && let Err(err) = create_network_and_gateway(
                        pool,
                        &host,
                        port,
                        GATEWAY_NAME,
                        result.cert_info,
                        result.core_client,
                    )
                    .await
                {
                    warn!(
                        "Gateway adoption TLS handshake succeeded but failed to persist \
                            network/gateway records: {err}"
                    );
                }
            }
            SetupAutoAdoptionComponent::Edge => {
                if let Some(result) = cert_info
                    && let Err(err) = create_proxy(
                        pool,
                        &host,
                        port,
                        PROXY_NAME,
                        result.cert_info,
                        result.core_client,
                    )
                    .await
                {
                    warn!(
                        "Edge adoption TLS handshake succeeded but failed to persist \
                            proxy record: {err}"
                    );
                }
            }
        }
    }

    let mut auto_state = AutoAdoptionWizardState::get(pool)
        .await
        .context("Failed to load auto-adoption wizard state")?
        .unwrap_or_default();

    auto_state.adoption_result.insert(
        component,
        AutoAdoptionComponentResult {
            success: status,
            logs: logs_to_persist(status, logs),
            updated_at: chrono::Utc::now().naive_utc(),
        },
    );
    auto_state.save(pool).await?;

    Ok(())
}

/// Creates a [`WireguardNetwork`] (location) pre-filled with auto-generated defaults and then
/// creates the associated [`Gateway`] record with the certificate data obtained during adoption.
async fn create_network_and_gateway(
    pool: &PgPool,
    host: &str,
    grpc_port: u16,
    common_name: &str,
    cert_info: CertificateInfo,
    core_client: CoreClientCert,
) -> Result<(), anyhow::Error> {
    // Re-use or create the network location.
    let network = if let Some(existing) = WireguardNetwork::find_by_name(pool, common_name)
        .await
        .context("Failed to query network by name")?
        .and_then(|mut v| {
            if v.is_empty() {
                None
            } else {
                Some(v.remove(0))
            }
        }) {
        info!(
            "Auto-adoption: reusing existing network location name={common_name} \
id={} for new gateway",
            existing.id
        );
        existing
    } else {
        let network_address: IpNetwork = DEFAULT_AUTO_ADOPTION_NETWORK_ADDRESS
            .parse()
            .context("Failed to parse default auto-adoption network address")?;

        let mut transaction = pool.begin().await.context("Failed to begin transaction")?;
        let network = WireguardNetwork::new(
            common_name.to_string(),
            DEFAULT_AUTO_ADOPTION_WIREGUARD_PORT,
            host.to_string(),
            None,
            Vec::new(),
            true,
            false,
            false,
            LocationMfaMode::Disabled,
            ServiceLocationMode::Disabled,
        )
        .set_address([network_address])?
        .save(&mut *transaction)
        .await
        .context("Failed to save auto-adopted WireguardNetwork")?;

        network
            .add_all_allowed_devices(&mut transaction)
            .await
            .context("Failed to assign IPs for existing devices in auto-adopted network")?;

        transaction
            .commit()
            .await
            .context("Failed to commit auto-adoption network transaction")?;

        info!(
            "Auto-adoption: created network location name={common_name} id={}",
            network.id
        );
        network
    };

    // Avoid duplicate gateway records for the same address:port.
    if let Some(existing) = Gateway::find_by_url(pool, host, grpc_port)
        .await
        .context("Failed to query existing gateways")?
    {
        info!(
            "Auto-adoption: gateway already registered at {host}:{grpc_port} (id={}); \
            skipping gateway record creation",
            existing.id
        );
        return Ok(());
    }

    let mut gateway = Gateway::new(
        network.id,
        common_name,
        host,
        i32::from(grpc_port),
        "Automatic setup",
    );
    gateway.certificate_serial = Some(cert_info.serial);
    gateway.certificate_expiry = Some(cert_info.not_after);
    gateway.core_client_cert_der = Some(core_client.cert_der);
    gateway.core_client_cert_key_der = Some(core_client.key_der);
    gateway.core_client_cert_expiry = Some(core_client.expiry);

    gateway
        .save(pool)
        .await
        .context("Failed to save auto-adopted Gateway")?;

    info!(
        "Auto-adoption: created gateway record name={common_name} address={host}:{grpc_port} \
        network_id={}",
        network.id
    );

    Ok(())
}

/// Creates a [`Proxy`] record in the database after a successful edge adoption.
async fn create_proxy(
    pool: &PgPool,
    host: &str,
    port: u16,
    common_name: &str,
    cert_info: CertificateInfo,
    core_client: CoreClientCert,
) -> Result<(), anyhow::Error> {
    if let Some(existing) = Proxy::find_by_address_port(pool, host, i32::from(port))
        .await
        .context("Failed to query existing proxies")?
    {
        info!(
            "Auto-adoption: proxy already registered at {host}:{port} (id={}); \
            skipping proxy record creation",
            existing.id
        );
        return Ok(());
    }

    let mut proxy = Proxy::new(common_name, host, i32::from(port), "Automatic setup");
    proxy.certificate_serial = Some(cert_info.serial);
    proxy.certificate_expiry = Some(cert_info.not_after);
    proxy.core_client_cert_der = Some(core_client.cert_der);
    proxy.core_client_cert_key_der = Some(core_client.key_der);
    proxy.core_client_cert_expiry = Some(core_client.expiry);

    proxy
        .save(pool)
        .await
        .context("Failed to save auto-adopted Proxy")?;

    info!("Auto-adoption: created proxy record name={common_name} address={host}:{port}");

    Ok(())
}

/// Stores and updates startup auto-adoption states for both components.
///
/// Both `config.adopt_edge` and `config.adopt_gateway` must be set before calling this
/// function. Callers (i.e. `main`) are responsible for enforcing that invariant.
pub async fn attempt_auto_adoption(
    pool: &PgPool,
    config: &DefGuardConfig,
) -> Result<(), anyhow::Error> {
    let (Some(edge_endpoint), Some(gateway_endpoint)) = (&config.adopt_edge, &config.adopt_gateway)
    else {
        anyhow::bail!(
            "Both --adopt-edge and --adopt-gateway must be set to run the auto-adoption wizard"
        );
    };

    let mut auto_state = AutoAdoptionWizardState::get(pool)
        .await
        .context("Failed to load auto-adoption wizard state")?
        .unwrap_or_default();

    let edge_already_succeeded = auto_state
        .adoption_result
        .get(&SetupAutoAdoptionComponent::Edge)
        .is_some_and(|result| result.success);
    let gateway_already_succeeded = auto_state
        .adoption_result
        .get(&SetupAutoAdoptionComponent::Gateway)
        .is_some_and(|result| result.success);

    if !edge_already_succeeded || !gateway_already_succeeded {
        ensure_ca_for_auto_adoption(pool).await?;
    }

    if edge_already_succeeded {
        info!(
            "Skipping startup auto-adoption for Edge component endpoint={edge_endpoint} as it was already completed"
        );
    } else {
        info!("Starting startup auto-adoption for Edge component endpoint={edge_endpoint}");
        if let Err(err) =
            process_startup_auto_adoption(pool, SetupAutoAdoptionComponent::Edge, edge_endpoint)
                .await
        {
            auto_state.adoption_result.insert(
                SetupAutoAdoptionComponent::Edge,
                AutoAdoptionComponentResult {
                    success: false,
                    logs: vec![format!("Startup auto-adoption failed: {err}")],
                    updated_at: chrono::Utc::now().naive_utc(),
                },
            );
            auto_state.save(pool).await?;
        } else {
            info!("Startup auto-adoption for Edge component completed endpoint={edge_endpoint}");
        }
    }

    if gateway_already_succeeded {
        info!(
            "Skipping startup auto-adoption for Gateway component endpoint={gateway_endpoint} as it was already completed"
        );
    } else {
        info!("Starting startup auto-adoption for Gateway component endpoint={gateway_endpoint}");
        if let Err(err) = process_startup_auto_adoption(
            pool,
            SetupAutoAdoptionComponent::Gateway,
            gateway_endpoint,
        )
        .await
        {
            auto_state.adoption_result.insert(
                SetupAutoAdoptionComponent::Gateway,
                AutoAdoptionComponentResult {
                    success: false,
                    logs: vec![format!("Startup auto-adoption failed: {err}")],
                    updated_at: chrono::Utc::now().naive_utc(),
                },
            );
            auto_state.save(pool).await?;
        } else {
            info!(
                "Startup auto-adoption for Gateway component completed endpoint={gateway_endpoint}"
            );
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::parse_host_port;

    #[test]
    fn test_parse_host_port_plain() {
        let (host, port) = parse_host_port("172.30.0.30:50052").unwrap();
        assert_eq!(host, "172.30.0.30");
        assert_eq!(port, 50052);
    }

    #[test]
    fn test_parse_host_port_strips_http_scheme() {
        let (host, port) = parse_host_port("http://172.30.0.30:50052").unwrap();
        assert_eq!(host, "172.30.0.30");
        assert_eq!(port, 50052);
    }

    #[test]
    fn test_parse_host_port_strips_https_scheme() {
        let (host, port) = parse_host_port("https://edge.example.com:50052").unwrap();
        assert_eq!(host, "edge.example.com");
        assert_eq!(port, 50052);
    }

    #[test]
    fn test_parse_host_port_domain_plain() {
        let (host, port) = parse_host_port("edge.example.com:50052").unwrap();
        assert_eq!(host, "edge.example.com");
        assert_eq!(port, 50052);
    }

    #[test]
    fn test_parse_host_port_ipv6_plain() {
        let (host, port) = parse_host_port("[::1]:50052").unwrap();
        assert_eq!(host, "::1");
        assert_eq!(port, 50052);
    }

    #[test]
    fn test_parse_host_port_ipv6_with_http_scheme() {
        // After stripping "http://", input becomes "[::1]:50052" which is valid IPv6 notation
        let (host, port) = parse_host_port("http://[::1]:50052").unwrap();
        assert_eq!(host, "::1");
        assert_eq!(port, 50052);
    }

    #[test]
    fn test_parse_host_port_missing_port_is_error() {
        assert!(parse_host_port("172.30.0.30").is_err());
        assert!(parse_host_port("http://172.30.0.30").is_err());
    }

    #[test]
    fn test_parse_host_port_invalid_port_is_error() {
        assert!(parse_host_port("172.30.0.30:99999").is_err());
        assert!(parse_host_port("http://172.30.0.30:notaport").is_err());
    }
}
