use std::convert::Infallible;
use std::sync::Arc;

use axum::Json;
use axum::extract::{Query, State};
use axum::http::{HeaderMap, StatusCode, header::AUTHORIZATION};
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize};
use tokio_stream::StreamExt as _;
use tokio_stream::wrappers::BroadcastStream;

use forage_engine::{ForageItemInput, SignalKind};

use crate::AppState;

// ── SSE event type ────────────────────────────────────────────────────────────

/// Payload pushed to feed pages over SSE when a new item is captured.
/// Contains everything `makeCard()` in the frontend needs to render a card
/// without an additional fetch.
#[derive(Clone, Serialize)]
pub struct CaptureEvent {
    pub item_id: u64,
    pub title: String,
    pub url: String,
    pub source: String,
    pub reading_time_min: u32,
    pub description: String,
    pub category: String,
    pub tags: Vec<String>,
    pub entities: Vec<String>,
    pub content_type: String,
    pub summary: String,
}

// ── Auth helper ───────────────────────────────────────────────────────────────

/// Check the `Authorization: Bearer <token>` header.
/// Returns `Ok(())` if auth is disabled (empty token) or the header matches.
/// Returns `Err((401, JSON))` otherwise — handlers return this directly.
fn auth_check(
    token: &str,
    headers: &HeaderMap,
) -> Result<(), (StatusCode, Json<serde_json::Value>)> {
    if token.is_empty() {
        return Ok(());
    }
    let expected = format!("Bearer {token}");
    let provided = headers.get(AUTHORIZATION).and_then(|v| v.to_str().ok());
    if provided == Some(expected.as_str()) {
        Ok(())
    } else {
        Err((
            StatusCode::UNAUTHORIZED,
            Json(serde_json::json!({"error": "unauthorized"})),
        ))
    }
}

// ── POST /signal ─────────────────────────────────────────────────────────────

#[derive(Deserialize)]
pub struct SignalReq {
    pub user_id: u64,
    pub item_id: u64,
    pub signal_type: String,
    pub duration_ms: Option<u64>,
}

pub async fn post_signal(
    State(state): State<Arc<AppState>>,
    headers: HeaderMap,
    Json(req): Json<SignalReq>,
) -> impl IntoResponse {
    if let Err(e) = auth_check(&state.token, &headers) {
        return e;
    }

    let result = match req.signal_type.as_str() {
        "dwell" => {
            let dur = req.duration_ms.unwrap_or(0);
            state.engine.signal_dwell(req.user_id, req.item_id, dur)
        }
        "view" => state
            .engine
            .signal(req.user_id, req.item_id, SignalKind::View),
        "save" => state
            .engine
            .signal(req.user_id, req.item_id, SignalKind::Save),
        "skip" => state
            .engine
            .signal(req.user_id, req.item_id, SignalKind::Skip),
        "share" => state
            .engine
            .signal(req.user_id, req.item_id, SignalKind::Share),
        other => {
            return (
                StatusCode::BAD_REQUEST,
                Json(serde_json::json!({"error": format!("unknown signal type: {other}")})),
            );
        }
    };

    match result {
        Ok(()) => (StatusCode::OK, Json(serde_json::json!({"ok": true}))),
        Err(e) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(serde_json::json!({"error": e.to_string()})),
        ),
    }
}

// ── POST /capture ─────────────────────────────────────────────────────────────
//
// Registers a discovered web page as a Forage item (idempotent) and fires a
// view signal for the given user.

#[derive(Deserialize)]
pub struct CaptureReq {
    pub url: String,
    /// Canonical URL from <link rel="canonical">.
    /// When non-empty and valid http(s), this is preferred over `url` so AMP
    /// variants and trailing-slash duplicates collapse to the same item.
    #[serde(default)]
    pub canonical_url: String,
    pub title: String,
    #[serde(default)]
    pub source: String,
    /// Content category hint.  Defaults to `"discovered"` when empty.
    #[serde(default)]
    pub category: String,
    #[serde(default)]
    pub description: String,
    #[serde(default = "default_reading_time")]
    pub reading_time_min: u32,
    #[serde(default = "default_user")]
    pub user_id: u64,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub entities: Vec<String>,
    #[serde(default)]
    pub content_type: String,
    #[serde(default)]
    pub summary: String,
}

fn default_reading_time() -> u32 {
    5
}

fn is_http_url(s: &str) -> bool {
    s.starts_with("http://") || s.starts_with("https://")
}

pub async fn post_capture(
    State(state): State<Arc<AppState>>,
    headers: HeaderMap,
    Json(req): Json<CaptureReq>,
) -> impl IntoResponse {
    if let Err(e) = auth_check(&state.token, &headers) {
        return e;
    }

    // Prefer canonical_url when it's a valid http(s) URL.
    let effective_url = if !req.canonical_url.is_empty() && is_http_url(&req.canonical_url) {
        req.canonical_url.clone()
    } else {
        req.url.clone()
    };

    // Reject empty or non-HTTP(S) URLs before they become phantom items.
    if effective_url.is_empty() || !is_http_url(&effective_url) {
        return (
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({
                "error": "url must be a valid http:// or https:// URL"
            })),
        );
    }
    if req.title.is_empty() {
        return (
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({"error": "title must not be empty"})),
        );
    }

    // Snapshot fields for the SSE broadcast before they're moved into ForageItemInput.
    let sse_url = effective_url.clone();
    let sse_title = req.title.clone();
    let sse_source = req.source.clone();
    let sse_category = req.category.clone();
    let sse_description = req.description.clone();
    let sse_reading_time_min = req.reading_time_min;
    let sse_tags = req.tags.clone();
    let sse_entities = req.entities.clone();
    let sse_content_type = req.content_type.clone();
    let sse_summary = req.summary.clone();

    let input = ForageItemInput {
        url: effective_url,
        title: req.title,
        source: req.source,
        category: req.category,
        description: req.description,
        reading_time_min: req.reading_time_min,
        tags: req.tags,
        entities: req.entities,
        content_type: req.content_type,
        summary: req.summary,
    };

    // add_item may call the embedding sidecar (blocking HTTP). Wrap in
    // spawn_blocking so the Tokio worker thread is not held during the round-trip.
    let engine = Arc::clone(&state.engine);
    let item_id = match tokio::task::spawn_blocking(move || engine.add_item(input)).await {
        Ok(Ok(id)) => id,
        Ok(Err(e)) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({"error": e.to_string()})),
            );
        }
        Err(e) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({"error": e.to_string()})),
            );
        }
    };

    // Always fire a view signal — revisiting a page is a valid engagement signal.
    if let Err(e) = state.engine.signal(req.user_id, item_id, SignalKind::View) {
        eprintln!("[forage-server] /capture: view signal failed for item {item_id}: {e}");
    }

    // Push the new item to any open SSE connections.  No connected clients is fine.
    let _ = state.events.send(CaptureEvent {
        item_id,
        title: sse_title,
        url: sse_url,
        source: sse_source,
        reading_time_min: sse_reading_time_min,
        description: sse_description,
        category: sse_category,
        tags: sse_tags,
        entities: sse_entities,
        content_type: sse_content_type,
        summary: sse_summary,
    });

    // Update discovery timestamp on every successful capture.
    *state.discovery.last_discovery_at.lock().await = Some(std::time::SystemTime::now());
    // Increment items count.
    let mut count = state.discovery.items_last_run.lock().await;
    *count += 1;

    (
        StatusCode::OK,
        Json(serde_json::json!({"item_id": item_id, "ok": true})),
    )
}

// ── GET /feed ────────────────────────────────────────────────────────────────

#[derive(Deserialize)]
pub struct FeedQuery {
    #[serde(default = "default_user")]
    pub user: u64,
    #[serde(default = "default_limit")]
    pub limit: usize,
}

fn default_user() -> u64 {
    1
}
fn default_limit() -> usize {
    7
}

pub async fn get_feed(
    State(state): State<Arc<AppState>>,
    headers: HeaderMap,
    Query(q): Query<FeedQuery>,
) -> impl IntoResponse {
    if let Err(e) = auth_check(&state.token, &headers) {
        return e;
    }
    let limit = q.limit.clamp(1, 50);
    match state.engine.feed(q.user, limit) {
        Ok(items) => (StatusCode::OK, Json(serde_json::json!({"items": items}))),
        Err(e) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(serde_json::json!({"error": e.to_string()})),
        ),
    }
}

// ── GET /prefs ───────────────────────────────────────────────────────────────

#[derive(Deserialize)]
pub struct PrefsQuery {
    #[serde(default = "default_user")]
    pub user: u64,
}

pub async fn get_prefs(
    State(state): State<Arc<AppState>>,
    headers: HeaderMap,
    Query(q): Query<PrefsQuery>,
) -> impl IntoResponse {
    if let Err(e) = auth_check(&state.token, &headers) {
        return e;
    }
    let categories = state.engine.top_categories(q.user);
    let has_preferences = !categories.is_empty();
    (
        StatusCode::OK,
        Json(serde_json::json!({
            "categories": categories,
            "has_preferences": has_preferences,
        })),
    )
}

// ── GET /items ───────────────────────────────────────────────────────────────

#[derive(Serialize)]
struct ItemSummary {
    id: u64,
    title: String,
    source: String,
    category: String,
    reading_time_min: u32,
    description: String,
    url: String,
}

pub async fn get_items(
    State(state): State<Arc<AppState>>,
    headers: HeaderMap,
) -> impl IntoResponse {
    if let Err(e) = auth_check(&state.token, &headers) {
        return e;
    }
    let items: Vec<ItemSummary> = state
        .engine
        .all_items()
        .iter()
        .map(|s| ItemSummary {
            id: s.id,
            title: s.title.clone(),
            source: s.source.clone(),
            category: s.category.clone(),
            reading_time_min: s.reading_time_min,
            description: s.description.clone(),
            url: s.url.clone(),
        })
        .collect();
    (StatusCode::OK, Json(serde_json::json!(items)))
}

// ── GET /events ───────────────────────────────────────────────────────────────
//
// SSE stream that pushes a CaptureEvent JSON blob every time a page is captured.
// `EventSource` in browsers cannot send custom headers, so auth uses a `?token=`
// query parameter instead of the `Authorization` header.

#[derive(Deserialize)]
pub struct EventsQuery {
    #[serde(default)]
    pub token: String,
}

pub async fn get_events(
    State(state): State<Arc<AppState>>,
    Query(params): Query<EventsQuery>,
) -> Response {
    if !state.token.is_empty() && params.token != state.token {
        return (StatusCode::UNAUTHORIZED, "unauthorized").into_response();
    }

    let rx = state.events.subscribe();
    let stream = BroadcastStream::new(rx).filter_map(|msg| match msg {
        Ok(event) => {
            let data = serde_json::to_string(&event).unwrap_or_default();
            Some(Ok::<Event, Infallible>(Event::default().data(data)))
        }
        // Lagged — receiver fell too far behind; drop the event.
        Err(_) => None,
    });

    Sse::new(stream)
        .keep_alive(KeepAlive::default())
        .into_response()
}

// ── GET /browse-tasks ─────────────────────────────────────────────────────────
//
// Returns a BrowsePlan telling the discovery agent which topics to browse,
// how many articles to capture per source, and tag hints from the user's
// engagement history.

#[derive(Deserialize)]
pub struct BrowseTasksQuery {
    #[serde(default)]
    pub prefer_tags: String,
}

pub async fn get_browse_tasks(
    State(state): State<Arc<AppState>>,
    headers: HeaderMap,
    Query(q): Query<BrowseTasksQuery>,
) -> impl IntoResponse {
    if let Err(e) = auth_check(&state.token, &headers) {
        return e;
    }
    let mut plan = state.engine.browse_tasks(1, 5);

    // Override should_run based on discovery recency. The engine always returns
    // true (it has no access to DiscoveryState), so we apply the gate here.
    let interval_secs = u64::from(plan.interval_minutes) * 60;
    {
        let last = state.discovery.last_discovery_at.lock().await;
        let count = *state.discovery.items_last_run.lock().await;
        if let Some(t) = *last
            && t.elapsed()
                .map(|e| e.as_secs() < interval_secs)
                .unwrap_or(false)
            && count >= 5
        {
            plan.should_run = false;
        }
    }

    // Merge prefer_tags into tag_hints (user-specified hints take priority).
    if !q.prefer_tags.is_empty() {
        let user_tags: Vec<String> = q
            .prefer_tags
            .split(',')
            .map(|t| t.trim().to_lowercase())
            .filter(|t| !t.is_empty())
            .collect();
        // Prepend user tags, then append engine tags that aren't already present.
        let existing: std::collections::HashSet<String> = user_tags.iter().cloned().collect();
        let mut merged = user_tags;
        for tag in plan.tag_hints {
            if !existing.contains(&tag) {
                merged.push(tag);
            }
        }
        plan.tag_hints = merged;
    }

    (
        StatusCode::OK,
        Json(serde_json::to_value(&plan).unwrap_or_default()),
    )
}

// ── POST /heartbeat ───────────────────────────────────────────────────────────
//
// Called periodically by the discovery agent to signal it is alive.

pub async fn post_heartbeat(
    State(state): State<Arc<AppState>>,
    headers: HeaderMap,
) -> impl IntoResponse {
    if let Err(e) = auth_check(&state.token, &headers) {
        return e;
    }
    *state.discovery.agent_last_seen.lock().await = Some(std::time::Instant::now());
    // Reset the per-cycle item counter. Heartbeat fires at the start of each
    // discovery cycle, so this tracks "items found in the most recent cycle."
    *state.discovery.items_last_run.lock().await = 0;
    (StatusCode::OK, Json(serde_json::json!({"ok": true})))
}

// ── GET /discovery/status ────────────────────────────────────────────────────
//
// Returns the current state of the autonomous discovery loop: whether the agent
// is connected, when the last run happened, how many items were found, and when
// the next run is expected.

pub async fn get_discovery_status(
    State(state): State<Arc<AppState>>,
    headers: HeaderMap,
) -> impl IntoResponse {
    if let Err(e) = auth_check(&state.token, &headers) {
        return e;
    }

    let interval_secs = 30 * 60u64; // 30 minutes

    let agent_connected = {
        let seen = state.discovery.agent_last_seen.lock().await;
        seen.map(|t| t.elapsed().as_secs() < 300).unwrap_or(false)
    };

    let (last_discovery_at_str, next_run_in_minutes) = {
        let last = state.discovery.last_discovery_at.lock().await;
        match *last {
            None => (serde_json::Value::Null, 0u64),
            Some(t) => {
                let elapsed = t.elapsed().unwrap_or_default().as_secs();
                let next = if elapsed >= interval_secs {
                    0
                } else {
                    (interval_secs - elapsed) / 60
                };
                let dt: chrono::DateTime<chrono::Utc> = t.into();
                (serde_json::Value::String(dt.to_rfc3339()), next)
            }
        }
    };

    let items_last_run = *state.discovery.items_last_run.lock().await;

    (
        StatusCode::OK,
        Json(serde_json::json!({
            "agent_connected": agent_connected,
            "last_discovery_at": last_discovery_at_str,
            "items_found_last_run": items_last_run,
            "next_run_in_minutes": next_run_in_minutes,
        })),
    )
}
