//! itsgoin.net web handler — serves shared posts by proxying content through
//! the anchor node. On-demand: connects to the author via QUIC, pulls the post,
//! renders HTML, serves blobs. No permanent storage of fetched content.
//!
//! Routes (behind Apache reverse proxy):
//!   GET /p/<postid_hex>/<author_hex>  → render post HTML (fetched on-demand)
//!   GET /b/<blobid_hex>               → serve blob (images/videos)

use std::net::SocketAddr;
use std::sync::Arc;

use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tracing::{debug, info, warn};

use crate::http::render_post_html;
use crate::node::Node;
use crate::types::{NodeId, PostId, PostVisibility};

/// Run the web handler on the given port. Blocks forever.
pub async fn run_web_handler(
    port: u16,
    node: Arc<Node>,
) -> anyhow::Result<()> {
    let addr: SocketAddr = ([127, 0, 0, 1], port).into();
    let listener = TcpListener::bind(addr).await?;
    info!("Web handler listening on {}", addr);

    loop {
        let (stream, _peer_addr) = match listener.accept().await {
            Ok(v) => v,
            Err(e) => {
                debug!("Web accept error: {}", e);
                continue;
            }
        };

        let node = Arc::clone(&node);
        tokio::spawn(async move {
            handle_web_request(stream, &node).await;
        });
    }
}

async fn handle_web_request(mut stream: TcpStream, node: &Arc<Node>) {
    let mut buf = vec![0u8; 4096];
    let n = match tokio::time::timeout(
        std::time::Duration::from_secs(5),
        stream.read(&mut buf),
    )
    .await
    {
        Ok(Ok(0)) | Ok(Err(_)) | Err(_) => return,
        Ok(Ok(n)) => n,
    };

    let request_bytes = &buf[..n];
    let (method, path) = match parse_request_line(request_bytes) {
        Some(v) => v,
        None => return,
    };

    if method != "GET" {
        return;
    }

    // Extract X-Forwarded-For header (set by Apache reverse proxy)
    let browser_ip = extract_header(request_bytes, "x-forwarded-for")
        .and_then(|v| v.split(',').next().map(|s| s.trim().to_string()));

    if path.starts_with("/p/") {
        serve_post(&mut stream, path, node, browser_ip.as_deref()).await;
    } else if path.starts_with("/b/") {
        serve_blob(&mut stream, path, node).await;
    }
}

/// Extract a header value from raw HTTP request bytes (case-insensitive).
fn extract_header<'a>(buf: &'a [u8], name: &str) -> Option<&'a str> {
    let text = std::str::from_utf8(buf).ok()?;
    let name_lower = name.to_lowercase();
    for line in text.split("\r\n") {
        if let Some(colon) = line.find(':') {
            if line[..colon].to_lowercase() == name_lower {
                return Some(line[colon + 1..].trim());
            }
        }
    }
    None
}

/// Handle GET /p/<postid_hex>/<author_hex>
///
/// Three-tier serving:
///   1. Redirect to a CDN holder with a public/punchable HTTP server
///   2. TCP hole-punch + redirect for EIM NAT holders
///   3. QUIC proxy fallback (fetch post + render HTML here)
async fn serve_post(stream: &mut TcpStream, path: &str, node: &Arc<Node>, browser_ip: Option<&str>) {
    let rest = &path[3..]; // strip "/p/"

    // Parse post_id (64 hex chars)
    if rest.len() < 64 {
        let _ = write_http_response(stream, 404, "text/plain", b"Not found").await;
        return;
    }
    let post_hex = &rest[..64];
    if !post_hex.chars().all(|c| c.is_ascii_hexdigit()) {
        return;
    }
    let post_id: PostId = match hex::decode(post_hex) {
        Ok(b) if b.len() == 32 => b.try_into().unwrap(),
        _ => return,
    };

    // Parse optional author_id (after the slash)
    let author_id: Option<NodeId> = if rest.len() > 65 {
        let author_hex = &rest[65..];
        if author_hex.len() == 64 && author_hex.chars().all(|c| c.is_ascii_hexdigit()) {
            hex::decode(author_hex).ok().and_then(|b| b.try_into().ok())
        } else {
            None
        }
    } else {
        None
    };

    // Gather all known holders: author + CDN downstream peers
    let (holders, local_post) = {
        let store = node.storage.lock().await;

        let mut holders = Vec::new();
        if let Some(author) = author_id {
            holders.push(author);
        }
        if let Ok(downstream) = store.get_post_downstream(&post_id) {
            for peer in downstream {
                if !holders.contains(&peer) {
                    holders.push(peer);
                }
            }
        }

        let local = store.get_post_with_visibility(&post_id).ok().flatten();
        (holders, local)
    };

    // --- Tier 1 & 2: Try direct redirect to an HTTP-capable holder ---
    if let Some(redirect_url) = try_redirect(node, &holders, &post_id, browser_ip).await {
        let header = format!(
            "HTTP/1.1 302 Found\r\nLocation: {}\r\nConnection: close\r\n\r\n",
            redirect_url
        );
        let _ = stream.write_all(header.as_bytes()).await;
        info!("Web: redirected post {} to {}", post_hex, redirect_url);
        return;
    }

    // --- Tier 3: QUIC proxy fallback ---

    // Check local storage first
    if let Some((post, visibility)) = local_post {
        if matches!(visibility, PostVisibility::Public) {
            let author_name = {
                let store = node.storage.lock().await;
                store.get_profile(&post.author).ok().flatten()
                    .map(|p| p.display_name).unwrap_or_default()
            };
            let html = render_post_html(&post, &post_id, &author_name);
            let _ = write_http_response(stream, 200, "text/html; charset=utf-8", html.as_bytes()).await;
            return;
        }
    }

    // Fetch via content search + PostFetch
    let author = author_id.unwrap_or([0u8; 32]);
    info!("Web: proxying post {} via QUIC (no redirect candidate found)", post_hex);

    let search_result = tokio::time::timeout(
        std::time::Duration::from_secs(15),
        fetch_post_from_network(node, &author, &post_id),
    ).await;

    match search_result {
        Ok(Ok(Some(sync_post))) => {
            {
                let store = node.storage.lock().await;
                let _ = store.store_post_with_visibility(
                    &sync_post.id, &sync_post.post, &sync_post.visibility,
                );
            }
            let author_name = {
                let store = node.storage.lock().await;
                store.get_profile(&sync_post.post.author).ok().flatten()
                    .map(|p| p.display_name).unwrap_or_default()
            };
            let html = render_post_html(&sync_post.post, &post_id, &author_name);
            let _ = write_http_response(stream, 200, "text/html; charset=utf-8", html.as_bytes()).await;
            return;
        }
        Ok(Ok(None)) => {
            debug!("Web: post not found via network search");
        }
        Ok(Err(e)) => {
            warn!("Web: network search failed: {}", e);
        }
        Err(_) => {
            warn!("Web: network search timed out (15s)");
        }
    }

    let html = render_unavailable_screen();
    let _ = write_http_response(stream, 200, "text/html; charset=utf-8", html.as_bytes()).await;
}

/// Try to redirect to an HTTP-capable holder of this post.
/// Returns a redirect URL (http://ip:port/p/<hex>) if a suitable holder is found.
///
/// Tier 1: Holders with http_capable=true and a known http_addr (publicly reachable).
/// Tier 2: Holders with EIM NAT — send TCP punch request, then redirect.
async fn try_redirect(
    node: &Arc<Node>,
    holders: &[NodeId],
    post_id: &PostId,
    browser_ip: Option<&str>,
) -> Option<String> {
    use crate::types::NatMapping;

    let post_hex = hex::encode(post_id);
    let store = node.storage.lock().await;

    // Classify holders into tiers
    let mut direct_candidates: Vec<(NodeId, String)> = Vec::new(); // http_addr known
    let mut punch_candidates: Vec<NodeId> = Vec::new(); // EIM NAT, connected

    for holder in holders {
        let (capable, addr) = store.get_peer_http_info(holder);
        if capable {
            if let Some(ref addr) = addr {
                direct_candidates.push((*holder, addr.clone()));
                continue;
            }
        }
        // Check if this peer has EIM NAT (punchable)
        let profile = store.get_peer_nat_profile(holder);
        if profile.mapping == NatMapping::EndpointIndependent {
            punch_candidates.push(*holder);
        }
    }
    drop(store);

    // Tier 1: Direct redirect to a publicly-reachable holder
    for (_holder, addr) in &direct_candidates {
        // Skip unroutable addresses (0.0.0.0, 127.x, etc.)
        if let Some(ip_str) = addr.split(':').next() {
            if let Ok(ip) = ip_str.parse::<std::net::IpAddr>() {
                if ip.is_unspecified() || ip.is_loopback() {
                    continue;
                }
            }
        }
        // Verify holder is actually connected (likely still alive)
        if node.network.is_connected(_holder).await
            || node.network.has_session(_holder).await
        {
            return Some(format!("http://{}/p/{}", addr, post_hex));
        }
    }

    // Tier 2: TCP punch + redirect for EIM NAT holders
    if let Some(browser_ip) = browser_ip {
        for holder in &punch_candidates {
            // Must be connected to send the punch request
            if !node.network.is_connected(holder).await
                && !node.network.has_session(holder).await
            {
                continue;
            }

            match node.network.tcp_punch(holder, browser_ip.to_string(), post_id).await {
                Ok(Some(http_addr)) => {
                    info!("Web: TCP punch succeeded for {}, redirecting to {}",
                        hex::encode(holder), http_addr);
                    return Some(format!("http://{}/p/{}", http_addr, post_hex));
                }
                Ok(None) => {
                    debug!("Web: TCP punch failed for {}", hex::encode(holder));
                }
                Err(e) => {
                    debug!("Web: TCP punch error for {}: {}", hex::encode(holder), e);
                }
            }
        }
    }

    None
}

/// Search the network for a post using extended worm search, then fetch it.
async fn fetch_post_from_network(
    node: &Arc<Node>,
    author: &NodeId,
    post_id: &PostId,
) -> anyhow::Result<Option<crate::protocol::SyncPost>> {
    // Step 1: Try direct connect to author + pull (fast path)
    if *author != [0u8; 32] {
        if let Ok(()) = node.connect_by_node_id(*author).await {
            // Try PostFetch directly from author
            if let Ok(Some(sp)) = node.network.post_fetch(author, post_id).await {
                return Ok(Some(sp));
            }
        }
    }

    // Step 2: Content search — worm with post_id
    let search = node.network.content_search(author, Some(*post_id), None).await?;

    if let Some(result) = search {
        // Try the post_holder first, then the found node
        let holders: Vec<NodeId> = [result.post_holder, Some(result.node_id)]
            .into_iter()
            .flatten()
            .collect();

        for holder in holders {
            // Connect if needed
            let _ = node.connect_by_node_id(holder).await;
            if let Ok(Some(sp)) = node.network.post_fetch(&holder, post_id).await {
                return Ok(Some(sp));
            }
        }
    }

    Ok(None)
}

/// Handle GET /b/<blobid_hex>
async fn serve_blob(stream: &mut TcpStream, path: &str, node: &Arc<Node>) {
    let blob_hex = &path[3..]; // strip "/b/"

    if blob_hex.len() != 64 || !blob_hex.chars().all(|c| c.is_ascii_hexdigit()) {
        return;
    }

    let blob_id: [u8; 32] = match hex::decode(blob_hex) {
        Ok(b) if b.len() == 32 => b.try_into().unwrap(),
        _ => return,
    };

    // Find which public post owns this blob and get the mime type + author.
    // Check blobs table first, then scan post attachments (for posts stored via PostFetch
    // which don't populate the blobs table).
    let (mime_type, author_id) = {
        let store = node.storage.lock().await;
        // Try blobs table first
        if let Some(mime) = find_public_blob_mime(&store, &blob_id) {
            let author = store.get_blob_post_id(&blob_id).ok().flatten().and_then(|pid| {
                store.get_post_with_visibility(&pid).ok().flatten().map(|(p, _)| p.author)
            });
            (mime, author)
        } else {
            // Scan recent posts for this blob CID in their attachments
            match find_blob_in_posts(&store, &blob_id) {
                Some((mime, author)) => (mime, Some(author)),
                None => return,
            }
        }
    };

    // Try local blob store first
    if let Ok(Some(data)) = node.blob_store.get(&blob_id) {
        let _ = write_http_response(stream, 200, &mime_type, &data).await;
        return;
    }

    // Blob not on disk — fetch from author via BlobRequest
    if let Some(author) = author_id {
        info!("Web: fetching blob {} from author {}", blob_hex, hex::encode(author));
        // Connect to author if needed
        let _ = node.connect_by_node_id(author).await;
        if let Ok(Some(blob_data)) = node.network.fetch_blob(&blob_id, &author).await {
            let _ = write_http_response(stream, 200, &mime_type, &blob_data).await;
            return;
        }
    }

    // Not found
    let _ = write_http_response(stream, 404, "text/plain", b"Blob not found").await;
}

/// Search post attachments for a blob CID. Returns (mime_type, author).
/// Used when the blobs table doesn't have an entry (e.g. posts stored via PostFetch).
fn find_blob_in_posts(store: &crate::storage::Storage, blob_id: &[u8; 32]) -> Option<(String, NodeId)> {
    store.find_blob_in_post_attachments(blob_id).ok()?
}

/// Find a blob's mime type, verifying it belongs to a public post.
fn find_public_blob_mime(store: &crate::storage::Storage, blob_id: &[u8; 32]) -> Option<String> {
    let post_id = store.get_blob_post_id(blob_id).ok()??;
    let (post, visibility) = store.get_post_with_visibility(&post_id).ok()??;
    if !matches!(visibility, PostVisibility::Public) {
        return None;
    }
    for att in &post.attachments {
        if att.cid == *blob_id {
            return Some(att.mime_type.clone());
        }
    }
    None
}

fn render_unavailable_screen() -> String {
    r##"<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ItsGoin</title>
<style>body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d0d;color:#e0e0e0;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0}
.card{background:#1a1a1a;border-radius:16px;padding:2.5rem;max-width:400px;text-align:center}
.hex{font-size:3rem;margin-bottom:1rem}h2{color:#5b8def;margin:0 0 0.5rem;font-size:1.3rem}
p{color:#888;font-size:0.9rem;line-height:1.5;margin:0.5rem 0}
.btn{display:inline-block;padding:0.7rem 1.5rem;border-radius:8px;text-decoration:none;font-weight:600;font-size:0.9rem;margin-top:1rem;background:#5b8def;color:#fff}
.btn:hover{background:#4a7cde}</style></head><body>
<div class="card"><div class="hex">&#x2B21;</div>
<h2>This content isn't currently reachable.</h2>
<p>It may be available again when someone who has it comes back online.</p>
<a class="btn" href="https://itsgoin.com">Install ItsGoin to find it when it resurfaces</a>
</div></body></html>"##.to_string()
}

/// Parse "GET /path HTTP/1.x\r\n..." → ("GET", "/path")
fn parse_request_line(buf: &[u8]) -> Option<(&str, &str)> {
    let line_end = buf.iter().position(|&b| b == b'\r' || b == b'\n')?;
    let line = std::str::from_utf8(&buf[..line_end]).ok()?;
    let mut parts = line.split(' ');
    let method = parts.next()?;
    let path = parts.next()?;
    let version = parts.next()?;
    if !version.starts_with("HTTP/") {
        return None;
    }
    Some((method, path))
}

async fn write_http_response(stream: &mut TcpStream, status: u16, content_type: &str, body: &[u8]) -> bool {
    let status_text = match status {
        200 => "OK",
        404 => "Not Found",
        _ => "Error",
    };
    let header = format!(
        "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nAccess-Control-Allow-Origin: *\r\nConnection: close\r\n\r\n",
        status, status_text, content_type, body.len()
    );
    if stream.write_all(header.as_bytes()).await.is_err() {
        return false;
    }
    stream.write_all(body).await.is_ok()
}
