use std::sync::Arc;

use axum::extract::Query;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Json};
use serde::{Deserialize, Serialize};
use tracing::info;

use crate::consts::MAX_POIS_PER_REQUEST;
use crate::state::{AppState, POICategoryGroup};

use super::hexagons::write_json_escaped;
use super::parse::parse_bounds;

#[derive(Deserialize)]
pub struct POIParams {
    bounds: Option<String>,
    /// Comma-separated list of categories to filter by
    categories: Option<String>,
}

pub async fn get_pois(
    state: Arc<AppState>,
    Query(params): Query<POIParams>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    let bounds_str = params.bounds.ok_or((
        StatusCode::BAD_REQUEST,
        "bounds parameter is required".into(),
    ))?;

    let (south, west, north, east) = parse_bounds(&bounds_str)?;

    let categories_str = params.categories.clone();
    let category_filter: Option<rustc_hash::FxHashSet<String>> = params
        .categories
        .as_deref()
        .filter(|text| !text.is_empty())
        .map(|text| {
            text.split(',')
                .map(|part| part.trim().to_string())
                .collect()
        });

    let num_categories = category_filter.as_ref().map(|cats| cats.len()).unwrap_or(0);

    let json_body = tokio::task::spawn_blocking(move || {
        let t0 = std::time::Instant::now();
        let row_indices = state.poi_grid.query(south, west, north, east);

        // Collect matching row indices first, then sample randomly so the
        // subset covers the viewport uniformly instead of clustering in one area.
        let mut matching_rows: Vec<usize> = row_indices
            .iter()
            .filter_map(|&row_idx| {
                let row = row_idx as usize;
                if let Some(ref categories) = category_filter {
                    if !categories.contains(state.poi_data.category.get(row)) {
                        return None;
                    }
                }
                Some(row)
            })
            .collect();

        if matching_rows.len() > MAX_POIS_PER_REQUEST {
            let ratio = (matching_rows.len() / MAX_POIS_PER_REQUEST) as u32;
            let step = ratio.next_power_of_two();
            let mask = step - 1;
            matching_rows.retain(|&row| state.poi_data.priority[row] & mask == 0);
            if matching_rows.len() > MAX_POIS_PER_REQUEST {
                matching_rows.sort_unstable_by_key(|&row| state.poi_data.priority[row]);
                matching_rows.truncate(MAX_POIS_PER_REQUEST);
            }
        }

        // Write JSON directly to string buffer, avoiding intermediate POI allocations
        let mut buf = String::with_capacity(matching_rows.len() * 128);
        buf.push_str("{\"pois\":[");

        for (i, &row) in matching_rows.iter().enumerate() {
            if i > 0 {
                buf.push(',');
            }
            buf.push_str("{\"id\":\"");
            write_json_escaped(&mut buf, &state.poi_data.id[row]);
            buf.push_str("\",\"name\":\"");
            write_json_escaped(&mut buf, &state.poi_data.name[row]);
            buf.push_str("\",\"category\":\"");
            write_json_escaped(&mut buf, state.poi_data.category.get(row));
            buf.push_str("\",\"group\":\"");
            write_json_escaped(&mut buf, state.poi_data.group.get(row));
            buf.push_str("\",\"lat\":");
            buf.push_str(&state.poi_data.lat[row].to_string());
            buf.push_str(",\"lng\":");
            buf.push_str(&state.poi_data.lng[row].to_string());
            buf.push_str(",\"emoji\":\"");
            write_json_escaped(&mut buf, state.poi_data.emoji.get(row));
            buf.push_str("\"}");
        }

        buf.push_str("]}");

        let elapsed = t0.elapsed();
        info!(
            results = matching_rows.len(),
            candidates = row_indices.len(),
            categories = num_categories,
            categories_raw = categories_str.as_deref().unwrap_or("-"),
            ms = format_args!("{:.1}", elapsed.as_secs_f64() * 1000.0),
            "GET /api/pois"
        );

        buf
    })
    .await
    .map_err(|error| (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()))?;

    Ok(([("content-type", "application/json")], json_body))
}

#[derive(Serialize)]
pub struct POICategoriesResponse {
    groups: Vec<POICategoryGroup>,
}

pub async fn get_poi_categories(state: Arc<AppState>) -> Json<POICategoriesResponse> {
    let groups: Vec<POICategoryGroup> = state.poi_category_groups.clone();

    let total: usize = groups.iter().map(|group| group.categories.len()).sum();
    info!(
        count = total,
        groups = groups.len(),
        "GET /api/poi-categories"
    );

    Json(POICategoriesResponse { groups })
}
