//! Custom domain-håndtering for publiseringssamlinger.
//!
//! Tre ansvarsområder:
//! 1. Verify-domain callback for Caddy on-demand TLS (intern, uautentisert)
//! 2. DNS-validering ved registrering av custom domain
//! 3. Ruting: domene → samling → serve artikkel/forside/feed
//!
//! Ref: docs/concepts/publisering.md § "Custom domain-mekanisme"

use axum::{
    extract::{Query, State},
    http::{HeaderMap, StatusCode},
    response::Response,
};
use serde::Deserialize;
use sqlx::PgPool;
use std::net::ToSocketAddrs;
use uuid::Uuid;

use crate::publishing::{self, PublishingConfig};
use crate::rss;
use crate::AppState;

// Re-export for route handler parameter types
// (PageQuery og SearchQuery er definert i publishing-modulen)

// =============================================================================
// Verify-domain (Caddy on-demand TLS callback)
// =============================================================================

#[derive(Deserialize)]
pub struct VerifyDomainQuery {
    domain: String,
}

/// GET /internal/verify-domain?domain=mittmagasin.no
///
/// Caddy kaller dette endepunktet før den henter TLS-sertifikat.
/// Returnerer 200 hvis domenet tilhører en samling med publishing-trait,
/// 404 ellers. Uautentisert — kun tilgjengelig internt.
pub async fn verify_domain(
    State(state): State<AppState>,
    Query(query): Query<VerifyDomainQuery>,
) -> StatusCode {
    let domain = query.domain.to_lowercase().trim().to_string();

    if domain.is_empty() {
        return StatusCode::BAD_REQUEST;
    }

    // Sjekk at domenet er registrert i en publishing-trait
    match find_collection_by_domain(&state.db, &domain).await {
        Ok(Some(_)) => {
            tracing::info!(domain = %domain, "Domene verifisert for on-demand TLS");
            StatusCode::OK
        }
        Ok(None) => {
            tracing::debug!(domain = %domain, "Ukjent domene avvist");
            StatusCode::NOT_FOUND
        }
        Err(e) => {
            tracing::error!(domain = %domain, error = %e, "DB-feil ved domeneverifisering");
            StatusCode::INTERNAL_SERVER_ERROR
        }
    }
}

// =============================================================================
// DNS-validering
// =============================================================================

/// Serverens forventede IP (fra miljøvariabel eller default).
fn expected_server_ip() -> String {
    std::env::var("SERVER_IP").unwrap_or_else(|_| "157.180.81.26".to_string())
}

/// Validerer at domenet har DNS-oppføring som peker til serveren.
///
/// Sjekker A-record via system DNS resolver. Returnerer Ok(()) hvis
/// minst én A-record peker til serverens IP, Err med forklaring ellers.
pub fn validate_dns(domain: &str) -> Result<(), String> {
    let expected_ip = expected_server_ip();

    // Bruk system DNS resolver
    let lookup = format!("{domain}:443")
        .to_socket_addrs()
        .map_err(|e| format!("DNS-oppslag feilet for {domain}: {e}"))?;

    let resolved_ips: Vec<String> = lookup.map(|addr| addr.ip().to_string()).collect();

    if resolved_ips.is_empty() {
        return Err(format!(
            "Ingen DNS-oppføringer funnet for {domain}. \
             Opprett en A-record som peker til {expected_ip}."
        ));
    }

    if resolved_ips.iter().any(|ip| ip == &expected_ip) {
        Ok(())
    } else {
        Err(format!(
            "DNS for {domain} peker til {:?}, men forventet {expected_ip}. \
             Opprett en A-record som peker til {expected_ip}.",
            resolved_ips
        ))
    }
}

// =============================================================================
// Domene → samling-oppslag
// =============================================================================

#[allow(dead_code)]
struct DomainCollection {
    id: Uuid,
    title: Option<String>,
    publishing_config: PublishingConfig,
    has_rss: bool,
    slug: String,
}

/// Finn samling basert på custom_domain i publishing-trait.
async fn find_collection_by_domain(
    db: &PgPool,
    domain: &str,
) -> Result<Option<DomainCollection>, sqlx::Error> {
    let row: Option<(Uuid, Option<String>, serde_json::Value)> = sqlx::query_as(
        r#"
        SELECT id, title, metadata
        FROM nodes
        WHERE node_kind = 'collection'
          AND metadata->'traits'->'publishing'->>'custom_domain' = $1
        LIMIT 1
        "#,
    )
    .bind(domain)
    .fetch_optional(db)
    .await?;

    let Some((id, title, metadata)) = row else {
        return Ok(None);
    };

    let traits = metadata.get("traits");

    let publishing_config: PublishingConfig = traits
        .and_then(|t| t.get("publishing"))
        .cloned()
        .map(|v| serde_json::from_value(v).unwrap_or_default())
        .unwrap_or_default();

    let slug = publishing_config
        .slug
        .clone()
        .unwrap_or_else(|| "unknown".to_string());

    let has_rss = traits.and_then(|t| t.get("rss")).is_some();

    Ok(Some(DomainCollection {
        id,
        title,
        publishing_config,
        has_rss,
        slug,
    }))
}

// =============================================================================
// Custom domain serving: forside, artikkel, feed
// =============================================================================

/// Ekstraher domene fra Host-header.
fn extract_domain(headers: &HeaderMap) -> Result<String, StatusCode> {
    let host = headers
        .get("host")
        .and_then(|v| v.to_str().ok())
        .ok_or(StatusCode::BAD_REQUEST)?;
    Ok(normalize_host(host))
}

/// GET /custom-domain/index — forside for custom domain.
///
/// Caddy rewriter forespørsler fra custom domains hit.
/// Vi bruker Host-headeren til å finne samlingen.
pub async fn serve_custom_domain_index(
    State(state): State<AppState>,
    headers: HeaderMap,
) -> Result<Response, StatusCode> {
    let domain = extract_domain(&headers)?;

    let collection = find_collection_by_domain(&state.db, &domain)
        .await
        .map_err(|e| {
            tracing::error!(domain = %domain, error = %e, "DB-feil ved domene-oppslag");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .ok_or(StatusCode::NOT_FOUND)?;

    let slug = collection.slug;
    publishing::serve_index(
        State(state),
        axum::extract::Path(slug),
    )
    .await
}

/// GET /custom-domain/{article_id} — enkeltartikkel for custom domain.
pub async fn serve_custom_domain_article(
    State(state): State<AppState>,
    headers: HeaderMap,
    axum::extract::Path(article_id): axum::extract::Path<String>,
) -> Result<Response, StatusCode> {
    let domain = extract_domain(&headers)?;

    let collection = find_collection_by_domain(&state.db, &domain)
        .await
        .map_err(|e| {
            tracing::error!(domain = %domain, error = %e, "DB-feil ved domene-oppslag");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .ok_or(StatusCode::NOT_FOUND)?;

    let slug = collection.slug;
    publishing::serve_article(
        State(state),
        axum::extract::Path((slug, article_id)),
    )
    .await
}

/// GET /custom-domain/kategori/{tag} — kategori for custom domain.
pub async fn serve_custom_domain_category(
    State(state): State<AppState>,
    headers: HeaderMap,
    axum::extract::Path(tag): axum::extract::Path<String>,
    query: Query<publishing::PageQuery>,
) -> Result<Response, StatusCode> {
    let domain = extract_domain(&headers)?;
    let collection = find_collection_by_domain(&state.db, &domain)
        .await
        .map_err(|e| {
            tracing::error!(domain = %domain, error = %e, "DB-feil ved domene-oppslag");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .ok_or(StatusCode::NOT_FOUND)?;

    let slug = collection.slug;
    publishing::serve_category(
        State(state),
        axum::extract::Path((slug, tag)),
        query,
    )
    .await
}

/// GET /custom-domain/arkiv — arkiv for custom domain.
pub async fn serve_custom_domain_archive(
    State(state): State<AppState>,
    headers: HeaderMap,
    query: Query<publishing::PageQuery>,
) -> Result<Response, StatusCode> {
    let domain = extract_domain(&headers)?;
    let collection = find_collection_by_domain(&state.db, &domain)
        .await
        .map_err(|e| {
            tracing::error!(domain = %domain, error = %e, "DB-feil ved domene-oppslag");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .ok_or(StatusCode::NOT_FOUND)?;

    let slug = collection.slug;
    publishing::serve_archive(
        State(state),
        axum::extract::Path(slug),
        query,
    )
    .await
}

/// GET /custom-domain/sok — søk for custom domain.
pub async fn serve_custom_domain_search(
    State(state): State<AppState>,
    headers: HeaderMap,
    query: Query<publishing::SearchQuery>,
) -> Result<Response, StatusCode> {
    let domain = extract_domain(&headers)?;
    let collection = find_collection_by_domain(&state.db, &domain)
        .await
        .map_err(|e| {
            tracing::error!(domain = %domain, error = %e, "DB-feil ved domene-oppslag");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .ok_or(StatusCode::NOT_FOUND)?;

    let slug = collection.slug;
    publishing::serve_search(
        State(state),
        axum::extract::Path(slug),
        query,
    )
    .await
}

/// GET /custom-domain/om — om-side for custom domain.
pub async fn serve_custom_domain_about(
    State(state): State<AppState>,
    headers: HeaderMap,
) -> Result<Response, StatusCode> {
    let domain = extract_domain(&headers)?;
    let collection = find_collection_by_domain(&state.db, &domain)
        .await
        .map_err(|e| {
            tracing::error!(domain = %domain, error = %e, "DB-feil ved domene-oppslag");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .ok_or(StatusCode::NOT_FOUND)?;

    let slug = collection.slug;
    publishing::serve_about(
        State(state),
        axum::extract::Path(slug),
    )
    .await
}

/// GET /custom-domain/feed.xml — RSS/Atom for custom domain.
pub async fn serve_custom_domain_feed(
    State(state): State<AppState>,
    headers: HeaderMap,
) -> Result<Response, StatusCode> {
    let domain = extract_domain(&headers)?;

    let collection = find_collection_by_domain(&state.db, &domain)
        .await
        .map_err(|e| {
            tracing::error!(domain = %domain, error = %e, "DB-feil ved domene-oppslag");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .ok_or(StatusCode::NOT_FOUND)?;

    let slug = collection.slug;
    rss::generate_feed(
        State(state),
        axum::extract::Path(slug),
    )
    .await
}

/// Normaliser Host-header: fjern port og lowercase.
fn normalize_host(host: &str) -> String {
    host.split(':').next().unwrap_or(host).to_lowercase()
}

// =============================================================================
// Re-rendering ved domeneendring
// =============================================================================

/// Enqueue re-rendering av alle artikler + forside for en samling.
///
/// Kalles når custom_domain endres — canonical URL i rendret HTML
/// må oppdateres. Artiklene re-rendres via jobbkøen (ikke-blokkerende).
pub async fn rerender_collection_articles(
    db: &PgPool,
    collection_id: Uuid,
) -> Result<usize, sqlx::Error> {
    // Finn alle artikler som tilhører samlingen
    let article_ids: Vec<(Uuid,)> = sqlx::query_as(
        r#"
        SELECT e.source_id
        FROM edges e
        WHERE e.target_id = $1
          AND e.edge_type = 'belongs_to'
        "#,
    )
    .bind(collection_id)
    .fetch_all(db)
    .await?;

    let count = article_ids.len();

    // Enqueue render-jobb for hver artikkel
    for (article_id,) in &article_ids {
        let payload = serde_json::json!({
            "node_id": article_id.to_string(),
            "collection_id": collection_id.to_string(),
        });
        if let Err(e) = crate::jobs::enqueue(db, "render_article", payload, Some(collection_id), 3).await {
            tracing::error!(
                article_id = %article_id,
                collection_id = %collection_id,
                error = %e,
                "Kunne ikke enqueue render_article ved domeneendring"
            );
        }
    }

    // Enqueue render av forsiden
    let index_payload = serde_json::json!({
        "collection_id": collection_id.to_string(),
    });
    if let Err(e) = crate::jobs::enqueue(db, "render_index", index_payload, Some(collection_id), 4).await {
        tracing::error!(
            collection_id = %collection_id,
            error = %e,
            "Kunne ikke enqueue render_index ved domeneendring"
        );
    }

    tracing::info!(
        collection_id = %collection_id,
        article_count = count,
        "Re-rendering enqueued for domeneendring"
    );

    Ok(count)
}

// =============================================================================
// Tester
// =============================================================================

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

    #[test]
    fn normalize_host_strips_port() {
        assert_eq!(normalize_host("mittmagasin.no:443"), "mittmagasin.no");
        assert_eq!(normalize_host("mittmagasin.no"), "mittmagasin.no");
        assert_eq!(normalize_host("MITTMAGASIN.NO:8080"), "mittmagasin.no");
    }

    #[test]
    fn dns_validation_rejects_empty() {
        let result = validate_dns("");
        assert!(result.is_err());
    }

    #[test]
    fn dns_validation_rejects_nonexistent() {
        let result = validate_dns("this-domain-does-not-exist-synops-test.invalid");
        assert!(result.is_err());
    }
}
